Wednesday 9 September 2020

FAQ - Why isn't my component updating when I call StateHasChanged ?

Blazor updates the UI using a diffing algorith in the same way as other SPAs like Angular, React and Vue.

This means that it does not re-render an entire HTML page when something changes, but re-renders individual components in as efficient a way as possible.

Example

Let's assume we have a page structured like this:

Let's assume that some change takes place in Component B which causes a re-rendering. This means that the rendering process has to call the child components as well, so Component C will also re-render.

However, re-renders flow down, not up. Component A is unaffected as it's a parent. Component D is also unaffected, since it's a child of A, not B.

How to Change This?

In most cases this is perfectly fine, but there are situations where the changes in one component need to be reflected in another. A good example here is a shopping cart model.

Let's assume the diagram above is a shopping app. Component B is the list of products. Component D is the shopping cart, maybe at the top right of the page.

A user can click 'Add to cart' next to a product in B. This should add to the cart, which should then update to show the number of items and total cost.

Since the two components are separate, we need to use some form of inter-component communication. In Blazor there are several ways to do this, and Chris Sainty's excellent blog article provides some details on how this can be done. To summarise, there are three ways:

Events

Component B can raise an event when a product within the component is added to the cart. The parent Component A would then have to wire up to each of these events and then forward them to D (the cart) so it can update, and then call StateHasChanged(). This isn't a good way since it requires wiring code to make things work, resulting it a lot of tightly-coupled code. Imagine if the product list was component C - then we'd have to raise the event to add in C, wire up the event in B, pass the event up to A, which then informs D that a new item was added.

I would argue it's not a good approach to solving this problem.

Cascading Parameters

Blazor supports Cascading Parameters where a value (or object) in a higher-level component can be referenced in a child component. This doesn't need any intermediate component to know about or pass along the value. In our shopping cart example, we could create a <CascadingValue Value="Cart"> in Component A. This can then be accessed in Component B, C and D if they chose, by using the [CascadingParameter] attribute.

The product page would call the Cart.AddProduct() method. The cart UI, component D also references the Cart using the [CascadingParameter] attribute, and subscribes to an event raised by the cart when it changes. It can then call StateHasChanged() for Component D.

State Containers

State containers have the same approach as the cascading parameters discussed above, but we use Dependency Injection to obtain an instance of the state container.

In our Shopping Cart example we'd create a Cart instance and in both the product list and the cart UI component D, we'd inject it:

@inject ShoppingCart Cart;

The product list then calls Cart.AddProduct() and the cart UI component D listens for an event CartChanged, to trigger an update.

No comments:

Post a Comment

5 - Adding Blazor

So in the previous step 4, we had upgraded our application to ASP.NET Core 3.1, but we still had no actual Blazor anywhere in the system. Ho...