Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Delayed OnParametersSetAsync task triggers duplicate OnParametersSet of child components

I'm trying to avoid repetitive OnParametersSet events from triggering in child components when the parent page/component has a "long" running OnParametersSet event of its own. For example, here's a basic page with some child components.

@page "/test"

<Node>
    <Node>
        <Node></Node>
    </Node>
</Node>

@code {
    protected override async Task OnInitializedAsync()
    {
        Console.WriteLine("Page: OnInitializedAsync - start");
        await Task.Delay(2000);
        Console.WriteLine("Page: OnInitializedAsync - finish");
    }
}

The node component is very simple as well:

<div>Node: @GetHashCode()</div>
@ChildContent

@code {
    [Parameter] public RenderFragment ChildContent { get; set; }

    protected override void OnParametersSet()
    {
        Console.WriteLine("Node {0}: OnParametersSet", GetHashCode());
    }
}

Here is what I see in the console. Note that after the page has finished its OnParametersSet event, two of the three child components call OnParametersSet again.

Page: OnInitializedAsync - start
Node 924945978: OnParametersSet
Node 1026183343: OnParametersSet
Node 213373360: OnParametersSet
Page: OnInitializedAsync - finish
Node 924945978: OnParametersSet
Node 1026183343: OnParametersSet

Is this just a flaw with Blazor or is there a better way to avoid these extra events? The node components have nothing to do with the content on the page itself. In the real world I may have many child components, each that are trying to fetch data async so I want to prevent these extra events from firing.

The only workaround I've found is to wrap the nodes block with an @if(pageSetParametersEventHasFinished) statement that prevents the node components from initializing until the page is "ready".

like image 680
WaitsAtWork Avatar asked Dec 07 '25 11:12

WaitsAtWork


2 Answers

This is by design. Note that the 2 outer Nodes are rendered twice because they have ChildContent. The inner Node is already 'stable'.

Blazor 'errs on the safe side', it can't guarantee nothing changed in the ChildContent.

But OnParametersSet is (should be) a light operation, not a problem in itself. Worry about the Render action that follows it.

My best practice for parameters

  • keep a copy inside the component
  • only when the new value differs from the copy, do fetch-data or other work
  • for a heavy component, use ShouldRender() to minimize re-rendering.
    That looks like this
bool shouldRender = true;

protected override void OnAfterRender(bool firstRender)
{
   ...
   shouldRender = false;
}

protected override bool ShouldRender()
{
    return shouldRender;
}

protected override void OnParametersSet()
{        
    if (myCopy != Param)
    {
      myCopy = Param;      
      shouldRender = true;
      ...  // fetch or process data
    }
}

This is only worth the effort for components that are 'heavy' in rendering and/or in processing data.

like image 190
Henk Holterman Avatar answered Dec 11 '25 05:12

Henk Holterman


Went with an abstract component like the following that handles parameter changes.

public abstract class WatchComponent : ComponentBase
{
    private bool _isDirty = true;
    private bool _shouldRender = true;

    protected abstract Task OnParametersChangedAsync();

    protected override async Task OnParametersSetAsync()
    {
        if (!_isDirty) return;

        await OnParametersChangedAsync();
        _isDirty = false;
    }

    protected override void OnAfterRender(bool firstRender)
    {
        _shouldRender = false; // disable if child component has RenderFragment parameter
        Console.WriteLine("{0}: OnAfterRender", GetHashCode());
        base.OnAfterRender(firstRender);
    }

    protected override bool ShouldRender() => _shouldRender;

    protected void SetField<T>(ref T field, T value)
    {
        if (EqualityComparer<T>.Default.Equals(field, value)) return;
        Console.WriteLine("parameter changed from {0} to {1}", field, value);
        field = value;
        _isDirty = true;
        _shouldRender = true;
    }
}

Using this with the demo from above I tried the following and everything seemed to work.

Updated page:

@page "/test"

<Node Value="@_value1">
    <Node Value="@_value2">
        <Node></Node>
    </Node>
</Node>

@code {
    private string _value1 = "v1";
    private string _value2 = "v2";

    protected override async Task OnInitializedAsync()
    {
        Console.WriteLine("Page: OnInitializedAsync - start");
        await Task.Delay(2000);
        Console.WriteLine("Page: OnInitializedAsync - finish");
        _value1 = "v1.1";
        _value2 = "v2";
    }
}

Updated Node component:

@inherits WatchComponent

<div>Node: @GetHashCode(), Value="@Value"</div>

@ChildContent

@code {

    private string _value;
    [Parameter]
    public string Value
    {
        get => _value;
        set => SetField(ref _value, value);
    }

    [Parameter] public RenderFragment ChildContent { get; set; }

    protected override Task OnParametersChangedAsync()
    {
        // load something async here
        return Task.CompletedTask;
    }
}

The console output:

Page: OnInitializedAsync - start
parameter changed from  to v1
parameter changed from  to v2
833522111: OnAfterRender
854912238: OnAfterRender
668729564: OnAfterRender
Page: OnInitializedAsync - finish
parameter changed from v1 to v1.1
833522111: OnAfterRender
like image 45
WaitsAtWork Avatar answered Dec 11 '25 05:12

WaitsAtWork



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!