State Hasn't Changed? Why and when Blazor components re-render

April 12, 2023 · 7 minute read · Tags: blazor

Sooner or later, as you build out your Blazor UI, you’re going to hit a situation where your component isn’t re-rendering when you expect it to.

The result? You’re stuck staring at “stale” data, scratching your head as to why the value hasn’t updated.

Sometimes a quick call to StateHasChanged is all you need to get back up and running but what’s really going on behind the scenes?

Blazor components (technically called Razor Components… naming is hard) typically inherit from a class called ComponentBase.

ComponentBase has some default behaviour baked in to determine when your component will be re-rendered.

Here are the primary reasons your component will re-render:

  • After an event occurs (when invoking an event handler in the same component)
  • After applying an updated set of parameters (from a parent)
  • After applying an updated value for a cascading parameter
  • After a call to StateHasChanged

Let’s take each one in turn.

After an event occurs (when invoking an event handler in the same component)

Here’s the classic “Counter” component:

When the button is clicked:

  • The component’s IncrementCount method is invoked
  • currentCount is updated

This will trigger a re-render of the counter component.

Here’s the same component again, with a visualisation of the component tree so we can see when the component (re)renders.

Notice how the Counter component renders every time we click the button to increment the count?

This is the default behaviour for Blazor components.

After an event occurs (when invoking an event handler in the same component), the component will be rendered.

Here’s an example with two counters:

Notice how each counter only re-renders when its own button is clicked.

The event is handled within the specific instance of the Counter component. This means the component that houses (and handles) that event will re-render, but sibling or parent components won’t.

How it works “under the hood”

Let’s take a moment to look at why the Counter component (re)renders in this case.

At compile-time your Razor components are turned into plain old C# classes, and it’s these that are executed when your Blazor app runs.

Here’s a simplified representation of the generated code for our Counter component.

public class Counter : ComponentTreeBase
{
    private int currentCount = 0;
    
    protected override void BuildRenderTree(RenderTreeBuilder __builder)
    {
      __builder.OpenElement(0, "div");
      __builder.AddAttribute(1, "b-r568j3cswt");
      __builder.AddMarkupContent(2, "<h3 b-r568j3cswt>Counter</h3>\r\n    \r\n    ");
      __builder.OpenElement(3, "button");
      __builder.AddAttribute<MouseEventArgs>(4, "onclick", EventCallback.Factory.Create<MouseEventArgs>((object) this, new Action(this.IncrementCount)));
      __builder.AddAttribute(5, "b-r568j3cswt");
      __builder.AddContent(6, "Click me");
      __builder.CloseElement();
      __builder.AddMarkupContent(7, "\r\n\r\n    ");
      __builder.OpenElement(8, "p");
      __builder.AddAttribute(9, "b-r568j3cswt");
      __builder.AddContent(10, "Count is: ");
      __builder.AddContent(11, (object) this.currentCount);
      __builder.CloseElement();
      __builder.CloseElement();
    }
    
    private void IncrementCount() => ++this.currentCount;
}

This is how Blazor builds up a render tree for our component.

Take a closer look at the generated code for the button.

__builder.OpenElement(3, "button");
__builder.AddAttribute<MouseEventArgs>(4, "onclick", EventCallback.Factory.Create<MouseEventArgs>((object) this, new Action(this.IncrementCount)));
__builder.AddAttribute(5, "b-r568j3cswt");
__builder.AddContent(6, "Click me");
__builder.CloseElement();

Notice how it directs the onclick event to an EventCallback.Factory.Create method.

Here’s the signature of that method:

EventCallback Create(object receiver, EventCallback callback) {}

Create returns an instance of EventCallback.

When the onclick event is triggered, this EventCallback will be invoked, which calls a method called HandleEventAsync on the receiver (Counter in this case).

Which leads us to this method in ComponentBase.

Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem callback, object? arg)
{
   ...

    // After each event, we synchronously re-render (unless !ShouldRender())
    // This just saves the developer the trouble of putting "StateHasChanged();"
    // at the end of every event callback.
    StateHasChanged();

    ...
}

Now in practice we don’t need to dig into those low level implementation details most of the time, as the default behaviour of Blazor components is usually what we want.

But it’s good to know why your component re-renders after handling an EventCallback, it’s all because of this call to StateHasChanged in ComponentBase.

Now, what about those other scenarios we mentioned…

After applying an updated set of parameters (from a parent)

Let’s take another scenario, where we have a parent component that passes a parameter to a child.

Here we’ve got one button which increments the _parentCount value, which is rendered in the parent component.

We then have another button (also in the parent) which increments the _childCount field, which is passed to the Child component (for it to display).

Let’s see how these components behave as we interact with each button…

To make things clearer, this version also highlights when the OnParametersSetAsync method is called for each component.

When you click the button to increment the parent count, only the parent component re-renders. This matches the rule we saw a moment ago (EventHandlers will trigger StateHasChanged).

But because the _childCount value hasn’t changed there’s no need for the Child component to be re-rendered.

On the other hand, when you click the button to increment the child count, both the parent and child components re-render.

This make sense, because the primitive value (_childCount which is of type int) being passed to the child component has changed (incremented) and so Blazor figures the child component will need to re-render.

Interestingly, this logic changes when we pass reference types as parameters.

Here’s the same example, but with a reference type for the child count.

This acts the same way as before (as far as the user is concerned) but the render behaviour is different, because of that reference type.

This might seem odd at first, when we click the button to increment the parent count the child component re-renders (even though its value hasn’t changed).

So what’s going on?

Well it turns out Blazor has no built-in change detection for reference types.

When the parent component handles the click event there’s a chance the child count’s value may have changed, so it will trigger a re-render of the child component.

Blazor will only skip this re-render if all the parameters are from a set of “known types” or primitive types that haven’t changed since the last time parameters were set (hence our earlier example, which used a primitive int for the count, behaved differently).

If you want to dig into what is a “known type” the change detection logic can be found here. Take a look at the IsKnownImmutableType method and you’ll see a very short list of types which are considered ‘known’.

After applying an updated value for a cascading parameter

You can use cascading parameters and values in your components to access state from a higher component.

Here’s an example:

Here we’ve a Home component which provides a cascading value of type UserDetails.

It renders a Dashboard component which, in turn, renders a UserPanel component.

The UserPanel component accepts a CascadingValue of type UserDetails, meaning it will have access to the value we send in via Home.

Now that button isn’t doing a lot (simply writing a message to the console), so what happens when we click it?

This is similar to the behaviour we saw with ‘regular’ parameters.

Blazor handles the click event and doesn’t know whether our UserDetails object has changed, so triggers a re-render of any components which receive it.

The Dashboard component doesn’t take any parameters so it isn’t re-rendered.

After a call to StateHasChanged

This one does exactly what it says on the tin!

When you explicitly call StateHasChanged in your component you instruct Blazor to perform a re-render.

In summary - Blazor is consistent about re-rendering

As we’ve seen in these examples, Blazor is pretty consistent when it comes to re-rendering.

If you trigger an event callback, the component which handles that callback will re-render, and child components will also be re-rendered if they accept parameters which fall outside the “known immutable types” list, including cascading parameters.

The main time you’re likely to need to call StateHasChanged yourself is if you’re performing operations outside of the component tree (and normal lifecycle of components) or want to render more times as you process data (for example to show a progress update).

Web development should be fun.

Write code, hit F5, view it in the browser and bask in the glory of a job well done.

But you're not basking… Why aren't you basking?!

Cut through all the noise and build better, simpler Blazor web applications with Practical Blazor Components.

Practical Blazor Components Build Better Blazor Web Apps, Faster

Next up

Finally! Improved Blazor Server reconnection UX
.NET 9 changes how your Blazor Server app behaves when server connection is lost
.NET 9 improves JavaScript module importing for Blazor
.NET 9 ensures your users always get the latest version of your JS modules
How to use .NET 9 to ensure users always get the latest version of your stylesheets
.NET 9 changes how static files are served, and it solves a long-standing problem