Samuel Carswell
Samuel Carswell

Reputation: 128

Blazor StateHasChanged and child parameters (`await Task.Run(StateHasChanged);` vs `await InvokeAsync(StateHasChanged);`)

I recently asked a question regarding the difference between await Task.Run(StateHasChanged); and await InvokeAsync(StateHasChanged); in Blazor wasm here.

The conclusion was that await Task.Run(StateHasChanged); was incorrect and should be avoided; using it would produce the same results as await InvokeAsync(StateHasChanged); however would fall over when threads are available (The accepted answer explains in detail).

I've updated my codebase to use await InvokeAsync(StateHasChanged);, however I've discovered there is actually a difference in outcome between the two.

Here's a minimal reproduction of the issue in my application:

Parent

<h1>Parent: @title</h1>

<button class="btn btn-primary" @onclick="() => SetBool(true)">True</button>
<button class="btn btn-primary" @onclick="() => SetBool(false)">False</button>

<Child Bool="@Bool" @ref="Child"></Child>

@code {
    private bool Bool = false;
    private Child Child;
    private string title;

    private async void SetBool(bool name)
    {
        Bool = name;
        title = Bool ? "True" : "False";
        // NOTE: This will work as expected; Child will be updated with the Parent
        // await Task.Run(StateHasChanged);
        // NOTE: This will only update the Child the second time it is clicked
        await InvokeAsync(StateHasChanged);
        await Child.Trigger();
    }

}

Child

<h3>Child: @title</h3>

@code {
    [Parameter]
    public bool Bool { set; get; } = false;
    private string title;

    public async Task Trigger()
    {
        title = Bool ? "True" : "False";
        await InvokeAsync(StateHasChanged);
    }
}

Clicking either the True or False button in the parent should update the Bool value in both the parent and child. Note that the title variable is only used for a visual display of the Bool value.

Using await Task.Run(StateHasChanged); will cause both the parent and child's state to be updated at the same time. On the other hand await InvokeAsync(StateHasChanged); will update the parent, but not the child; it takes two clicks of the a button to get the respective value in the child component.

Is there an issue with how I'm passing this value to the child component?

Note that using await Task.Run(StateHasChanged); isn't an option; doing so means I can't test the component with bUnit.

The reproduction code is available here.

Upvotes: 1

Views: 3010

Answers (4)

MrC aka Shaun Curtis
MrC aka Shaun Curtis

Reputation: 30001

await Task.Run(StateHasChanged);

is a NONO. It says, run StateHasChanged on a threadpool thread, which is the exact opposite of reality - StateHasChanged must be run on the UI Thread.

It "works" in single threaded environments because Task.Run doesn't switch threads - there's only the UI thread. In Web Assembly that's the case at the moment. Run it in any multi-threaded environment - Blazor Server or bUnit testing - and it goes bang.

Blazor provides two InvokeAsync methods on ComponentBase to run methods on the UI Thread. One for an Action and one for a Func. Here's the Action version.

protected Task InvokeAsync(Action workItem)
    => _renderHandle.Dispatcher.InvokeAsync(workItem);

RenderHandle is a struct that the Renderer passes to the component when it calls Attach. Dispatcher is the Thread Dispatcher for the UI thread. InvokeAsync ensures whatever Action or Func it's passed, it gets run on the UI Thread. In our case StateHasChanged.

The answer to the behaviour differences in your code is answered by both Enet and Henk above. I would just re-iterate Enet's comment on async and void. My personal rule is: no async and void together in a Blazor Component event handler.

You will find a significant number of questions on this specific subject on here (Stack Overflow), and many answers by one of the three of us!

Upvotes: 0

Henk Holterman
Henk Holterman

Reputation: 273169

You have an async void in async void SetBool(bool name). That explains the difference you see with Task.Run.

  1. You will (almost) never need async void in Blazor. Blazor supports awaitable eventhandlers. Use async Task SetBool(bool name) ... and the results should become deterministic.

  2. You don't need the InvokeAsync() here. Everything runs on the main thread.
    You will only need it with Blazor Server in code that is executed by Task.Run(). Or in an async void but that shouldn't happen.

  3. You don't need StateHasChanged() everywhere. Only use it to display intermediate results in a method. In the sample code you would kinda need it in Trigger but currently Trigger() itself is not needed. The Child should rerender when Bool changes without any help from you. The async void may have led you to believe otherwise.

You are trying too hard. The whole sample can be simplified to

Parent

<h1>Parent: @title</h1>

<button class="btn btn-primary" @onclick="() => SetBool(true)">True</button>
<button class="btn btn-primary" @onclick="() => SetBool(false)">False</button>

<Child Bool="@Bool" ></Child>

@code {
    private bool Bool = false;
   // private Child Child;
    private string title;

    private void SetBool(bool name)  // no async
    {
        Bool = name;   // Child will rerender if this is a change
        title = Bool ? "True" : "False";
    }  // auto StateHasChanged

}

Child

<h3>Child: @title</h3>

@code 
{
    [Parameter]
    public bool Bool { set; get; } = false;
    private string title => Bool ? "True" : "False";
}

Upvotes: 1

enet
enet

Reputation: 45586

The following code snippet describes how you should code:

Parent.razor

<h1>Parent: @title</h1>

<button class="btn btn-primary" @onclick="() => SetBool(true)">True</button>
<button class="btn btn-primary" @onclick="() => SetBool(false)">False</button>

<Child Bool="@Bool" @ref="Child"></Child>

@code {
    private bool Bool = false;
    private Child Child;
    private string title;

    protected override void OnInitialized()
    {
        title = Bool ? "True" : "False";
    }

    private async Task SetBool(bool name)
    {
        Bool = name;
        title = Bool ? "True" : "False";
        
        await Task.CompletedTask;
    }

}

Child.razor

<h3>Child: @title</h3>

@code {
    [Parameter]
    public bool Bool { set; get; } = false;
    private string title;

    private bool _Bool;

    public async Task Trigger()
    {
        title = _Bool ? "True" : "False";
        await InvokeAsync(StateHasChanged);
    }

    protected override async Task OnParametersSetAsync()
    {
        _Bool = Bool;
        await Trigger();
      
    }
}

Note: The issues you're facing is not because of using Task.Run(StateHasChanged); and await InvokeAsync(StateHasChanged);, though the result is different. You simply don't follow the Blazor Component Model rules.

  1. Do you realize that when you click on the SetBool button, the Parent component is re-rendered, as its state has changed. Consequently, the Child component is re-rendered with Bool == true, but you don't see that on the screen as the code of changing the title in the child component is placed in the Trigger method, which is only called from the Parent component. Your assumption was wrong.

  2. Do not modify or change the State of the Bool parameter property. Define a local variable to store the value of the parameter property, whose value you may manipulate as you wish. In other words, the component parameter properties should be defined as automatic properties... They are a DTO for the Blazor framework to pass values from a parent component to its children.

  3. Do not call the StateHasChanged method from the UI event handlers unnecessarily. It is called automatically.

  4. Do not use async void... use async Task. When you use async void, Blazor doesn't figure out when the an async method completes, and thus it does not call the StateHasChanged method

  5. Avoid using Task.Run. I never use. I don't even remember why I shouldn't use it. I just don't use it.

Upvotes: 4

Steve Wong
Steve Wong

Reputation: 2256

You aren't passing the new value into the Child component. This is one way you could make it work.

private async void SetBool(bool name)
{
    ...
                
    await InvokeAsync(StateHasChanged);
    await Child.Trigger(Bool); // pass the new value
}

public async Task Trigger(bool newValue)
{
    title = newValue ? "True" : "False";
    await InvokeAsync(StateHasChanged);
}

Upvotes: 0

Related Questions