Avoid overwriting parameters in ASP.NET Core Blazor

Note

This isn't the latest version of this article. For the current release, see the .NET 8 version of this article.

Important

This information relates to a pre-release product that may be substantially modified before it's commercially released. Microsoft makes no warranties, express or implied, with respect to the information provided here.

For the current release, see the .NET 8 version of this article.

By Robert Haken

This article explains how to avoid overwriting parameters in Blazor apps during rerendering.

Overwritten parameters

The Blazor framework generally imposes safe parent-to-child parameter assignment:

  • Parameters aren't overwritten unexpectedly.
  • Side effects are minimized. For example, additional renders are avoided because they may create infinite rendering loops.

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:

  • The child component is rendered with one or more parameter values from the parent component.
  • The child writes directly to the value of a parameter.
  • The parent component rerenders and overwrites the value of the child's parameter.

The potential for overwriting parameter values extends into the child component's property set accessors, too.

Important

Our general guidance is not to create components that directly write to their own parameters after the component is rendered for the first time.

Consider the following ShowMoreExpander component that:

  • Renders the title.
  • Shows the child content when selected.
  • Allows you to set initial state with a component parameter (InitiallyExpanded).

After the following ShowMoreExpander component demonstrates an overwritten parameter, a modified ShowMoreExpander component is shown to demonstrate the correct approach for this scenario. The following examples can be placed in a local sample app to experience the behaviors described.

ShowMoreExpander.razor:

<div @onclick="ShowMore" class="card bg-light mb-3" style="width:30rem">
    <div class="card-header">
        <h2 class="card-title">Show more (<code>Expanded</code> = @InitiallyExpanded)</h2>
    </div>
    @if (InitiallyExpanded)
    {
        <div class="card-body">
            <p class="card-text">@ChildContent</p>
        </div>
    }
</div>

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

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

    private void ShowMore()
    {
        InitiallyExpanded = true;
    }
}
<div @onclick="ShowMore" class="card bg-light mb-3" style="width:30rem">
    <div class="card-header">
        <h2 class="card-title">Show more (<code>Expanded</code> = @InitiallyExpanded)</h2>
    </div>
    @if (InitiallyExpanded)
    {
        <div class="card-body">
            <p class="card-text">@ChildContent</p>
        </div>
    }
</div>

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

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

    private void ShowMore()
    {
        InitiallyExpanded = true;
    }
}
<div @onclick="ShowMore" class="card bg-light mb-3" style="width:30rem">
    <div class="card-header">
        <h2 class="card-title">Show more (<code>Expanded</code> = @InitiallyExpanded)</h2>
    </div>
    @if (InitiallyExpanded)
    {
        <div class="card-body">
            <p class="card-text">@ChildContent</p>
        </div>
    }
</div>

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

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

    private void ShowMore()
    {
        InitiallyExpanded = true;
    }
}
<div @onclick="ShowMore" class="card bg-light mb-3" style="width:30rem">
    <div class="card-header">
        <h2 class="card-title">Show more (<code>Expanded</code> = @InitiallyExpanded)</h2>
    </div>
    @if (InitiallyExpanded)
    {
        <div class="card-body">
            <p class="card-text">@ChildContent</p>
        </div>
    }
</div>

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

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

    private void ShowMore()
    {
        InitiallyExpanded = true;
    }
}
<div @onclick="ShowMore" class="card bg-light mb-3" style="width:30rem">
    <div class="card-header">
        <h2 class="card-title">Show more (<code>Expanded</code> = @InitiallyExpanded)</h2>
    </div>
    @if (InitiallyExpanded)
    {
        <div class="card-body">
            <p class="card-text">@ChildContent</p>
        </div>
    }
</div>

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

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

    private void ShowMore()
    {
        InitiallyExpanded = true;
    }
}

The ShowMoreExpander component is added to the following Expanders parent component that may call StateHasChanged:

Expanders.razor:

@page "/expanders"

<PageTitle>Expanders</PageTitle>

<h1>Expanders Example</h1>

<ShowMoreExpander InitiallyExpanded="false">
    Expander 1 content
</ShowMoreExpander>

<ShowMoreExpander InitiallyExpanded="false" />

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

<PageTitle>Expanders</PageTitle>

<h1>Expanders Example</h1>

<ShowMoreExpander InitiallyExpanded="false">
    Expander 1 content
</ShowMoreExpander>

<ShowMoreExpander InitiallyExpanded="false" />

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

<PageTitle>Expanders</PageTitle>

<h1>Expanders Example</h1>

<ShowMoreExpander InitiallyExpanded="false">
    Expander 1 content
</ShowMoreExpander>

<ShowMoreExpander InitiallyExpanded="false" />

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

<h1>Expanders Example</h1>

<ShowMoreExpander InitiallyExpanded="false">
    Expander 1 content
</ShowMoreExpander>

<ShowMoreExpander InitiallyExpanded="false" />

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

<h1>Expanders Example</h1>

<ShowMoreExpander InitiallyExpanded="false">
    Expander 1 content
</ShowMoreExpander>

<ShowMoreExpander InitiallyExpanded="false" />

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

Initially, the ShowMoreExpander components behave independently when their InitiallyExpanded properties are set. The child components maintain their states as expected.

If StateHasChanged is called in a parent component, the Blazor framework rerenders child components if their parameters might have changed:

  • For a group of parameter types that Blazor explicitly checks, Blazor rerenders a child component if it detects that any of the parameters have changed.
  • For unchecked parameter types, Blazor rerenders the child component regardless of whether or not the parameters have changed. Child content falls into this category of parameter types because child content is of type RenderFragment, which is a delegate that refers to other mutable objects.

For the Expanders component:

  • The first ShowMoreExpander component sets child content in a potentially mutable RenderFragment, so a call to StateHasChanged in the parent component automatically rerenders the component and potentially overwrites the value of InitiallyExpanded to its initial value of false.
  • The second ShowMoreExpander component doesn't set child content. Therefore, a potentially mutable RenderFragment doesn't exist. A call to StateHasChanged in the parent component doesn't automatically rerender the child component, so the component's InitiallyExpanded value isn't overwritten.

To maintain state in the preceding scenario, use a private field in the ShowMoreExpander component to maintain its state.

The following revised ShowMoreExpander component:

  • Accepts the InitiallyExpanded component parameter value from the parent.
  • Assigns the component parameter value to a private field (expanded) in the OnInitialized event.
  • Uses the private field to maintain its internal toggle state, which demonstrates how to avoid writing directly to a parameter.

Note

The advice in this section extends to similar logic in component parameter set accessors, which can result in similar undesirable side effects.

ShowMoreExpander.razor:

<div @onclick="Expand" class="card bg-light mb-3" style="width:30rem">
    <div class="card-header">
        <h2 class="card-title">Show more (<code>Expanded</code> = @expanded)</h2>
    </div>
    @if (expanded)
    {
        <div class="card-body">
            <p class="card-text">@ChildContent</p>
        </div>
    }
</div>

@code {
    private bool expanded;

    [Parameter]
    public bool InitiallyExpanded { get; set; }

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

    protected override void OnInitialized()
    {
        expanded = InitiallyExpanded;
    }

    private void Expand()
    {
        expanded = true;
    }
}
<div @onclick="Expand" class="card bg-light mb-3" style="width:30rem">
    <div class="card-header">
        <h2 class="card-title">Show more (<code>Expanded</code> = @expanded)</h2>
    </div>
    @if (expanded)
    {
        <div class="card-body">
            <p class="card-text">@ChildContent</p>
        </div>
    }
</div>

@code {
    private bool expanded;

    [Parameter]
    public bool InitiallyExpanded { get; set; }

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

    protected override void OnInitialized()
    {
        expanded = InitiallyExpanded;
    }

    private void Expand()
    {
        expanded = true;
    }
}
<div @onclick="Expand" class="card bg-light mb-3" style="width:30rem">
    <div class="card-header">
        <h2 class="card-title">Show more (<code>Expanded</code> = @expanded)</h2>
    </div>
    @if (expanded)
    {
        <div class="card-body">
            <p class="card-text">@ChildContent</p>
        </div>
    }
</div>

@code {
    private bool expanded;

    [Parameter]
    public bool InitiallyExpanded { get; set; }

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

    protected override void OnInitialized()
    {
        expanded = InitiallyExpanded;
    }

    private void Expand()
    {
        expanded = true;
    }
}
<div @onclick="Expand" class="card bg-light mb-3" style="width:30rem">
    <div class="card-header">
        <h2 class="card-title">Show more (<code>Expanded</code> = @expanded)</h2>
    </div>
    @if (expanded)
    {
        <div class="card-body">
            <p class="card-text">@ChildContent</p>
        </div>
    }
</div>

@code {
    private bool expanded;

    [Parameter]
    public bool InitiallyExpanded { get; set; }

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

    protected override void OnInitialized()
    {
        expanded = InitiallyExpanded;
    }

    private void Expand()
    {
        expanded = true;
    }
}
<div @onclick="Expand" class="card bg-light mb-3" style="width:30rem">
    <div class="card-header">
        <h2 class="card-title">Show more (<code>Expanded</code> = @expanded)</h2>
    </div>
    @if (expanded)
    {
        <div class="card-body">
            <p class="card-text">@ChildContent</p>
        </div>
    }
</div>

@code {
    private bool expanded;

    [Parameter]
    public bool InitiallyExpanded { get; set; }

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

    protected override void OnInitialized()
    {
        expanded = InitiallyExpanded;
    }

    private void Expand()
    {
        expanded = true;
    }
}

Note

The revised ShowMoreExpander doesn't reflect changes to the InitiallyExpanded parameter after initialization (OnInitialized). In certain scenarios, an already initialized component might receive new parameter values. This can happen, for example, in a master-detail view where the same component is used to render different detail views or when the /item/{id} route parameter changes to display a different item.

Consider following ToggleExpander component that:

  • Allows you to change the state both from inside and outside.
  • Handles new parameter values even if the same component instance is reused.

ToggleExpander.razor:

<div class="card bg-light mb-3" style="width:30rem">
    <div @onclick="Toggle" class="card-header">
        <h2 class="card-title">Toggle (<code>Expanded</code> = @expanded)</h2>
    </div>
    @if (expanded)
    {
        <div class="card-body">
            <p class="card-text">@ChildContent</p>
        </div>
    }
</div>

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

    [Parameter]
    public EventCallback<bool> ExpandedChanged { get; set; }

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

    private bool expanded;

    protected override void OnParametersSet()
    {
        expanded = Expanded;
    }

    private async void Toggle()
    {
        expanded = !expanded;
        await ExpandedChanged.InvokeAsync(expanded);
    }
}
<div class="card bg-light mb-3" style="width:30rem">
    <div @onclick="Toggle" class="card-header">
        <h2 class="card-title">Toggle (<code>Expanded</code> = @expanded)</h2>
    </div>
    @if (expanded)
    {
        <div class="card-body">
            <p class="card-text">@ChildContent</p>
        </div>
    }
</div>

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

    [Parameter]
    public EventCallback<bool> ExpandedChanged { get; set; }

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

    private bool expanded;

    protected override void OnParametersSet()
    {
        expanded = Expanded;
    }

    private async void Toggle()
    {
        expanded = !expanded;
        await ExpandedChanged.InvokeAsync(expanded);
    }
}
<div class="card bg-light mb-3" style="width:30rem">
    <div @onclick="Toggle" class="card-header">
        <h2 class="card-title">Toggle (<code>Expanded</code> = @expanded)</h2>
    </div>
    @if (expanded)
    {
        <div class="card-body">
            <p class="card-text">@ChildContent</p>
        </div>
    }
</div>

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

    [Parameter]
    public EventCallback<bool> ExpandedChanged { get; set; }

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

    private bool expanded;

    protected override void OnParametersSet()
    {
        expanded = Expanded;
    }

    private async void Toggle()
    {
        expanded = !expanded;
        await ExpandedChanged.InvokeAsync(expanded);
    }
}
<div class="card bg-light mb-3" style="width:30rem">
    <div @onclick="Toggle" class="card-header">
        <h2 class="card-title">Toggle (<code>Expanded</code> = @expanded)</h2>
    </div>
    @if (expanded)
    {
        <div class="card-body">
            <p class="card-text">@ChildContent</p>
        </div>
    }
</div>

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

    [Parameter]
    public EventCallback<bool> ExpandedChanged { get; set; }

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

    private bool expanded;

    protected override void OnParametersSet()
    {
        expanded = Expanded;
    }

    private async void Toggle()
    {
        expanded = !expanded;
        await ExpandedChanged.InvokeAsync(expanded);
    }
}
<div class="card bg-light mb-3" style="width:30rem">
    <div @onclick="Toggle" class="card-header">
        <h2 class="card-title">Toggle (<code>Expanded</code> = @expanded)</h2>
    </div>
    @if (expanded)
    {
        <div class="card-body">
            <p class="card-text">@ChildContent</p>
        </div>
    }
</div>

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

    [Parameter]
    public EventCallback<bool> ExpandedChanged { get; set; }

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

    private bool expanded;

    protected override void OnParametersSet()
    {
        expanded = Expanded;
    }

    private async void Toggle()
    {
        expanded = !expanded;
        await ExpandedChanged.InvokeAsync(expanded);
    }
}

The ToggleExpander component should be used with the @bind-Expanded="{field}" binding syntax, allowing two-way synchronization of the parameter.

ExpandersToggle.razor:

@page "/expanders-toggle"

<PageTitle>Expanders Toggle</PageTitle>

<h1>Expanders Toggle</h1>

<ToggleExpander @bind-Expanded="expanded">
    Expander content
</ToggleExpander>

<button @onclick="Toggle">Toggle</button>

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

@code {
    private bool expanded;

    private void Toggle()
    {
        expanded = !expanded;
    }
}
@page "/expanders-toggle"

<PageTitle>Expanders Toggle</PageTitle>

<h1>Expanders Toggle</h1>

<ToggleExpander @bind-Expanded="expanded">
    Expander content
</ToggleExpander>

<button @onclick="Toggle">Toggle</button>

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

@code {
    private bool expanded;

    private void Toggle()
    {
        expanded = !expanded;
    }
}
@page "/expanders-toggle"

<PageTitle>Expanders Toggle</PageTitle>

<h1>Expanders Toggle</h1>

<ToggleExpander @bind-Expanded="expanded">
    Expander content
</ToggleExpander>

<button @onclick="Toggle">Toggle</button>

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

@code {
    private bool expanded;

    private void Toggle()
    {
        expanded = !expanded;
    }
}
@page "/expanders-toggle"

<h1>Expanders Toggle</h1>

<ToggleExpander @bind-Expanded="expanded">
    Expander content
</ToggleExpander>

<button @onclick="Toggle">Toggle</button>

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

@code {
    private bool expanded;

    private void Toggle()
    {
        expanded = !expanded;
    }
}
@page "/expanders-toggle"

<h1>Expanders Toggle</h1>

<ToggleExpander @bind-Expanded="expanded">
    Expander content
</ToggleExpander>

<button @onclick="Toggle">Toggle</button>

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

@code {
    private bool expanded;

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

For more information on parent-child binding, see the following resources:

For more information on change detection, including information on the exact types that Blazor checks, see ASP.NET Core Razor component rendering.