Home > Back-end >  Due to Blazor Change Detection API, component re-rendering get skipped. How to avoid?
Due to Blazor Change Detection API, component re-rendering get skipped. How to avoid?

Time:01-12

I am working on Blazor project (.NET 5). I got a problem with components rendering. I have parent component with ChildContent as RenderFragment inside. And I use it like this:

<ParentComponent>
    <ChildComponent1 Title="Component1"></ChildComponent1>
    <ChildComponent2 Title="Component2" SampleEnum="SampleEnum.Bar"></ChildComponent2>
</ParentComponent>

Each ChildComponent inherits ChildComponentBase:

public class ChildComponent1 : ChildComponentBase 
{
   // some code
}

ChildComponentBase contains ParentComponent as cascading parameter and 2 parameters: one of them is string (Immutable for Blazor Change Detection API) and another one is enum (which is not Immutable) just for sake of example. And here we also

public partial class ChildComponentBase
{
     [CascadingParameter]
     public ParentComponent Parent { get; set; } = default !;

     [Parameter]
     public string? Title { get; set; } // Immutable

     [Parameter]
     public SampleEnum SampleEnum { get; set; } // not Immutable
}

In ParentComponent I use a strategy of deferred rendering. Defer component looks like this and being used in ParentComponent:

// This is used to move its body rendering to the end of the render queue so we can collect
// the list of child components first.
public class Defer : ComponentBase
{
    [Parameter]
    public RenderFragment ChildContent { get; set; }

    protected override void BuildRenderTree( RenderTreeBuilder builder )
    {
        builder.AddContent( 0, ChildContent );
    }
}

In my project on first render I collect all ChildComponent from ChildContent like this:

ChildComponentBase.razor

@{
    Parent.AddChild(this); // Parent is cascading parameter
}

And then I invoke a callback to process data. ParentComponent looks like this: ParentComponent.razor

<CascadingValue Value="this" IsFixed>
    @{
        StartCollectingChildren();
    }
    @ChildContent

    <Defer>
        @{
            FinishCollectingChildren();

            ProcessDataAsync();
        }

        @foreach (var o in _childComponents)
        {
            <p>@o.Title</p>
        }
    </Defer>
</CascadingValue>

ParentComponent.razor.cs

public partial class ParentComponent
{
    [Parameter]
    public RenderFragment ChildContent { get; set; }

    private List<ChildComponentBase> _childComponents = new();
    private bool _firstRender = true;
    private bool _collectingChildren; // Children might re-render themselves arbitrarily. We only want to capture them at a defined time.

    protected async Task ProcessDataAsync()
    {
        if (_firstRender)
        {
            //imitating re-render just like it would be an async call
            await InvokeAsync(StateHasChanged);
            _firstRender = false;
        }
    }

    public void AddChild(ChildComponentBase child)
    {
        _childComponents.Add(child);
    }

    private void StartCollectingChildren()
    {
        _childComponents.Clear();
        _collectingChildren = true;
    }

    private void FinishCollectingChildren()
    {
        _collectingChildren = false;
    }
}

Due to invoke of callback - re-rendering happens. And due to re-rendering StartCollectingChildren() is getting called again. This time on second render of ParentComponent the ChildComponent1 doesn't re-render, because Blazor Change Detection API skips it (because it contains only an Immutable parameter Title while ChildComponent2 in addition contains enum parameter).

Question: how to make this ChildComponent1 get re-rendered anyway?

I also added a Sample Project with code described above for you to try it out yourself.

I tried everything I could find in the google. The best workaround I found is to cache children collection on first render, but it looks dirty and could cause issues in a future.

CodePudding user response:

The quick fix to your problem is to modify the cascade and remove IsFixed.

Once you do that any component that captures the cascade will always be rendered because this is an object and therefore fails the equality check.

You can also drive render events on sub components that don't have object parameters using a Guid Cascade. Assign a new Guid to the mapped parameter whenever you want to force a render on any sub component the captures the cascade.

  • Related