How do properties cause pages to update in Blazor?

Viewed 1147

I've just finished my first non-trivial test app in Blazor WebAssembly. Blazor is impressive stuff, but I don't find it easy to reason about how changes to properties cause updates to the DOM - for example, where the property is referenced in a Razor component.

<div>@SomeProperty</div>
public int SomeProperty {get;set;}

In WPF, it's very easy to reason about how changes flow and cause rendering changes because they're triggered by events and DependencyProperty changes. You can see these and bind to them. In Blazor, somehow you can change a property value and the page updates. The precise mechanism behind this is a bit like magic. As a result, it can be hard to reason about how to remove unnecessary updates for complex components.

Can anyone explain the underpinnings of this topic?

Is there any documentation or video that goes into depth on this area of Blazor?

2 Answers

Can anyone explain the underpinnings of this topic?

To understand the update process you need to understand Components. I'll try and keep this short!

All components must implement IComponent. ComponentBase is an implementation of IComponent.

public interface IComponent
{
    void Attach(RenderHandle renderHandle);
    Task SetParametersAsync(ParameterView parameters);
}

The important bits of a RenderHandle are:

public readonly struct RenderHandle
{
    public Dispatcher Dispatcher ....
    //....
    public void Render(RenderFragment renderFragment)
    {
        //....
        _renderer.AddToRenderQueue(_componentId, renderFragment);
        //...
    }
}    

And a RenderFragment is:

public delegate void RenderFragment(RenderTreeBuilder builder);

A Renderer manages the render process. It holds The DOM (what gets rendered by the Browser) represented as a RenderTree. When the Renderer attaches a component to the RenderTree, the Renderer creates a RenderHandle and passes it to the component by calling Attach. The Component uses this RenderHandle to communicate with the Renderer. The Renderer communicates with the component by calling SetParametersAsync.

Components "Render" by calling the Render method on the RenderHandle and passing a RenderFragment delegate.

Here's a simple render fragment:

protected RenderFragment HelloWorld => (RenderTreeBuilder builder) =>
{
    builder.OpenElement(0, "div");
    builder.AddContent(1, "Hello Blazor");
    builder.CloseElement();
};

Calling Render on the RenderHandle DOES NOT render the component. It just places the render fragment on the Renderer's queue. When the Renderer runs the fragment it checks for component parameter changes referenced by other components. It calls SetParametersAsync on any components where their reference parameters have changed.

StateHasChanged is a ComponentBase method. StateHasChanged is called internally by the Blazor UI event handler, so you rarely need to call it manually. If you do, ask yourself why? Your logic may be wrong! It looks like this:

var task = InvokeAsync(EventMethod);
StateHasChanged();
if (!task.IsCompleted)
{
    await task;
    StateHasChanged();
}

The main exception is a normal event handler. If the event updates data in the component you need to trigger a manual update by calling StateHasChanged. This is the standard pattern.

private void OnSomethingChanged(object? sender, EventArgs e)
    => this.InvokeAsync(StateHasChanged);

Notes:

  1. StateHasChanged has a mechanism for detecting if a render fragment is already queued, so it doesn't queue multiple renders.
  2. InvokeAsync ensures a task is run on the UI thread. It uses the Dispatcher provided on the RenderHandle.
  3. When the Renderer checks for parameter changes, any object is considered dirty as there's no simple way for the Renderer to check equality.
  4. The Renderer only services it's queue when it gets thread time to do so. If you run a long sequence of synchronous code in say a button click handler nothing happens till the synchronous code completes.

Delve into ComponentBase if you want to dig further - You can view the code here.

In Blazor, somehow you can change a property value and the page updates.

Not exactly. The property itself has nothing to do with this. You can for instance update it with a Timer and you won't see the UI changing.

But usually you will set the property in response to a ButtonClick or other Blazor (lifecycle) event. And those are bracketed with calls to StateHasChanged(). In a Timer event or other non-Blazor event you will have to call StateHasChanged() yourself.

Blazor maintains its own (fast) copy of the DOM. After StateHasChanged() that copy is rebuilt and compared to the previous one. Any changes are applied to the actual DOM with JavaScript.

As a result, it can be hard to reason about how to remove unnecessary updates for complex components.

That is different from <div>@SomeProperty</div>.

When you have <Details ItemId="itemId" /> and itemId is a basic type like int or a string then Details will only be re-rendered when itemId changes.

But when it is <Details Item="item" /> and item is some complex type then the component will always be rerendered along with the parent page.

I have sometimes used the following pattern in the Details component, when Item has no mutable properties, ie for <Details Item="selectedItem" />:

[Parameter]
public Item Item { get; set; } = new();

int oldId = 0;

protected override bool ShouldRender()
{
    if (oldId != Item.Id)
    {
        oldId = Item.Id;
        return true;
    }
    return false;
}
Related