72

I am having a hard time understanding when I should call StateHasChanged() and when Blazor intercepts that something is changed so it must be re-rendered.

I've created a sample project with a button and a custom component called AddItem. This component contains a div with a red border and a button.

What I expected: I want that the AddItem's div will show up when the user clicks on the button contained inside the Index page. Then I want to hides it when the user clicks on AddItem's button.

Note: AddItem doesn't expose it _isVisible flag outside, instead it contains a Show() method. So AddItems.Show() will be invoked when the Index's button is clicked.

Tests:

  1. I click on Index's click button then the methods Open() and AddItem.Show() are invoked. The flag _isVisible is set to true but nothing happens and Index's ShouldRender() is invoked.

    Console output:

    • Render Index
  2. I've modified AddItem.Show() with public void Show() {_isVisible = true; StateHasChanged();}. Now the AddItem's div shows and hide as expected.

    Console output:

    • Render AddItem (1° click on index's button)
    • Render Index (1° click on index's button)
    • Render AddItem (2° click on addItem's close button)
  3. I've modified <AddItem @ref="AddItem" /> with <AddItem @ref="AddItem" CloseEventCallback="CallBack" />, removed StateHasChanged from AddItem's Show() method. Now the AddItem's div shows and hides as expected.

Based on Test 3: Why I don't have to explicit StateHasChanged if I set AddItem's CloseEventCallback to any parent's method? I'm having a hard time understanding it because AddItem doesn't invoke CloseEventCallback anywhere.

and

When Blazor understands that something is changed so it must be re-render?

My sample code (if you want to try it).

My Index.razor

<AddItem @ref="AddItem" />
<button @onclick="Open">click</button>
@code {
    AddItem AddItem;

    public void Open()
    {
        AddItem.Show();
    }

    public void CallBack()
    {
    }

    protected override bool ShouldRender()
    {
        Console.WriteLine("Render INDEX");
        return base.ShouldRender();
    }
}

My AddItem component

@if (_visible)
{
    <div style="width: 100px; height: 100px; border: 1px solid red">testo</div>
    <button @onclick="Close">close</button>    
}

@code {
    private bool _visible = false;

    [Parameter] public EventCallback<bool> CloseEventCallback { get; set; }

    public void Show()
    {
        _visible = true;
    }

    public void Close()
    {
        _visible = false;
    }

    protected override bool ShouldRender()
    {
        Console.WriteLine("Render ADDITEM");
        return base.ShouldRender();
    }
}
Leonardo Lurci
  • 2,409
  • 3
  • 18
  • 34

2 Answers2

37

Generally speaking, the StateHasChanged() method is automatically called after a UI event is triggered, as for instance, after clicking a button element, the click event is raised, and the StateHasChanged() method is automatically called to notify the component that its state has changed and it should re-render.

When the Index component is initially accessed. The parent component renders first, and then the parent component renders its child.

Whenever the "Open" button is clicked the Index component re-renders (This is because the target of the event is the parent component, which by default will re-render (No need to use StateHasChanged). But not the child, who is not aware that his state has changed. In order to make the child aware that his state has changed and that it should re-render, you should add a call to the StateHasChanged method manually in the Show method. Now, when you click on the "Open" button, the child component is re-rendered first, and then its parent re-renders next. Now the red div is rendered visible.

Click the "Close" button to hide the red div. This time only the child component re-renders (This is because the target of the event is the child component, and it re-renders by default), but not the parent.

This behavior is correct and by design.

If you remove the call to the StateHasChanged method from the AddItem.Show method, define this property: [Parameter] public EventCallback<bool> CloseEventCallback { get; set; }, and add a component attribute in the parent component to assign a value to this property like this: <AddItem @ref="AddItem" CloseEventCallback="CallBack" />, you'll notice no change outwardly, but this time the order of re-rendering when the "Open" button is clicked, is first the parent re-renders, then the child re-renders. This describes exactly the issue you've found expressed in your question from the comments:

So, why my test 3 worked as expected even if CloseEventCallback isn't invoked anywhere?

You are right... I could not really explain this behvior before having a further investigation. I'll try to find out what is going on, and let you know.

AddItem's close method invoke the CloseEventCallback to advise the parent that it should re-render.

Note: your code define the CloseEventCallback with a boolean type specifier, so you must define a method in your parent component that has a boolean parameter. When you invoke the CloseEventCallback 'delegate' you actually call the Index.Callback method and you should pass it a boolean value. Naturally, if you passes a value to a component, you expect it to re-render so that the new state can be seen in the UI. And this is the functionality that the EventCallback provides: Though the event is triggered in the child component, its target is the parent component, which results in the parent component re-rendering.

I am wondering why a parent component should re-render itself if one of the subscribed EventCallback is invoked?

This is exactly what I'm trying to explain in the paragraph above. The EventCallback type was especially design to solve the issue of the event target, routing the event to the component whose state has changed (the parent component), and re-rendering it.

enet
  • 41,195
  • 5
  • 76
  • 113
  • Thank you Enet, now it is more clear to me. There are still two questions in my mind. 1. As expected from Blazor Docs, AddItem's close method invoke the CloseEventCallback to advise the parent that it should re-render. So, why my test 3 worked as expected even if CloseEventCallback isn't invoked anywhere? 2. I am wondering why a parent component should re-render itself if one of the subscribed EventCallback is invoked? I might have an EventCallback that isn't strictly related to UI state. I know that this is a weird question, I just ask because I want to understand – Leonardo Lurci Feb 06 '20 at 15:38
  • 1
    @Leonardo Lurci , I've updated my answer to provide for your questions in the comment above. This is a very complicated subject... Please, don't hesitate to ask questions. – enet Feb 07 '20 at 00:18
  • Thank you. I didn't know that there is a specific order during the rendering. I agree with you when you wrote `When you invoke the CloseEventCallback [...] you expect it to re-render`, indeed my was a weird question just to understand what is going on under the Blazor engine. I would ask you, if possible, where did you find this information: looking asp.net Core Blazor on GitHub? I am trying to learn Blazor and I would like to understand more than "how can you use Blazor / what can you do with Blazor". Do you have any kind of suggestions? Please, let me know if you find anything on my test 3. – Leonardo Lurci Feb 07 '20 at 08:43
  • 1
    My source of learning is the documents, which were very poor not long ago, answering question in stackoverflow, and the issues section of blazor in github. I do not visit other sources such as blogs, and I do not waste my time with third party's components. I purely concentrate around the Blazor component model and how to use it. I also learn a lots from the code samples created by the Blazor team, such as FlightFinder, Blazing pizzas, etc. – enet Feb 07 '20 at 10:05
  • 2
    The best material is provided by Steve Anderson... just go to his repositories, read his samples, analyze them, run them, etc. Here is a link to an issue provided by rynowak, one of the Blazor team, about EventCallback. Make sure you read it all: https://github.com/dotnet/aspnetcore/issues/6351 – enet Feb 07 '20 at 10:05
8

Components must render when they're first added to the component hierarchy by a parent component. This is the only time that a component must render. Components may render at other times according to their own logic and conventions.

SPA applications follow the following architecture of components:

a nodes-graph representing SPA architecture, with a root node and leaves, some nodes being parents and some being children

See each node in that tree as a component of ours in the Blazor application. This component has a parent component, and can have child components.

A call to StateHasChanged is received by ComponentBase (standard base class for any Blazor component). This is the one that contains the logic to trigger re-rendering at the following times automatically:

  • After applying an updated set of parameters from a parent component.
  • After applying an updated value for a cascading parameter.
  • After notification of an event and invoking one of its own event handlers.
  • After a call to its own StateHasChanged method (see ASP.NET Core Razor component lifecycle).

Unlike an automatic rendering call for Blazor, in our case the rendering call is coming from StateHasChanged. This has a different behavior:
When the framework itself renders on one of the aforementioned times, it renders only its subcomponents (children) from it.
But when we call StateHasChanged, most components that inherit from ComponentBase are rendered, regardless of their position in the tree. (An unnecessary re-rendering).
Therefore, we should avoid using StateHasChanged, letting the framework handle rendering.

Occasions when it makes sense to call StateHasChanged:

  • An asynchronous handler involving multiple asynchronous phases.
  • Receiving a call from something external to the Blazor rendering and event-handling systems.
  • To render a component outside the subtree, that is re-rendered by a particular event.

To learn more:

ANeves
  • 6,219
  • 3
  • 39
  • 63
IuriBaltieri
  • 91
  • 1
  • 3