Search code examples
blazorblazor-webassembly

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".


Solution

  • 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