Home > Net >  Understanding Overwritten Parameters problem in Blazor
Understanding Overwritten Parameters problem in Blazor

Time:09-22

I'm learning Blazor by reading through Microsoft's Blazor documentation and have problem understanding this example on Overwritten Parameters problem. Here is the example code:

Child component:

<div @onclick="Toggle" class="card bg-light mb-3" style="width:30rem">
    <div class="card-body">
        <h2 class="card-title">Toggle (<code>Expanded</code> = @Expanded)</h2>

        @if (Expanded)
        {
            <p class="card-text">@ChildContent</p>
        }
    </div>
</div>

@code {
    [Parameter]
    public bool Expanded { get; set; }

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

    private void Toggle()
    {
        Expanded = !Expanded;
    }
}

Parent:

@page "/expander-example"

<Expander Expanded="true">
    Expander 1 content
</Expander>

<Expander Expanded="true" />

<button @onclick="StateHasChanged">
    Call StateHasChanged
</button>

What's confusing me is:

  1. Why is the second Expander not re-rendered and the parameter binding does not work either?

  2. What exactly is the problem the documentation tries to show here? When a parent component reset its state, I would expect the change to be reflected on the child components, which is exactly what the first supposedly problematic expander does.

CodePudding user response:

The issue is described in this paragraph.

A child component receives new parameter values that possibly overwrite existing values when the parent component rerenders. Accidentally overwriting parameter values in a child component often occurs when developing the component with one or more data-bound parameters and the developer writes directly to a parameter in the child

If you don't want child component to depend on parent property, don't pass this property as a parameter to the child. To make example explicit, they trigger StateHasChanged method to initiate change detection on the parent page. This, in turn, triggers rerendering of the child, which may not be a desired behavior if you want to keep child expander open. This is why they recommend to use private variable inside child to isolate its state from the parent.

This is NOT the biggest issue though :)

What MSDN doesn't tell you is that working with Blazor, sooner or later you will realize that it's change detection policy is significantly different compared to widely used SPA frameworks, like Angular or React. These frameworks detect only changes of the internal state (class variables) defined in the component. On the contrary, Blazor likes to re-render entire page and all its children even in some arbitrary cases, e.g. when you clicked at some random place on the page.

How do I stop blazor @onclick re-rendering page

In other words, you may see the same issue even if you don't call StateHasChanged explicitly.

CodePudding user response:

  1. Why is the second Expander not re-rendered and the parameter binding does not work either?

Because there is one parameter of a simple value type and a reference type (ChildContent) that is null. The render engine decides "nothing has changed here" and skips it.

That that boolean parameter was changed 'from the inside' is precisely the problem of 'overwritten parameters'.

The first <Expander> has a non-null ChildContent and then the engine does not look any deeper, it assumes that that is a mutable object and rerenders the component.

Note that that means there is a hidden cost in passing complex objects instead of valuetypes. <PersonCard PersonId="person.Id" /> will not be re-rendered as often as <PersonCard Person="person" />

CodePudding user response:

What exactly is the problem the documentation tries to show here? When a parent component reset its state, I would expect the change to be reflected on the child components, which is exactly what the first supposedly problematic expander does.

The objective is to warn against using public parameters to maintain the internal state of a component that you want to control only from the inside of the component.

Visualize a scenario where you have an editable Title and its details in the collapsed state. The model class Title also has some other properties (Skipping for brevity).

Consider the following:

<TitleEditor Title="@title" @OnTitleChanged="RefreshTitle"/>

<Details Content="@content" CollapseState="@areDetailsCollapsed"/>
    

@code
{
   TitleClass title = new ();
   ContentClass content = new ();
   bool areDetailsCollapsed = true;

   void RefreshTitle(string title)
   {
     title.Text = title;
     StateHasChanged();
   }
}

The user updates the title. This calls "RefreshTitle" which is intended to update only the Title. However, since the reference type parameters are involved, Details gets refreshed too. That will result in the internal collapse state of Details will be reset. Which is a wholly unintended side effect.

Further, StateHasChanged() cascade refreshes on every child component down the tree. Thus any StateHasChanged() anywhere in any parent node will cause a component to losing its state.

  • Related