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.
Warning
This version of ASP.NET Core is no longer supported. For more information, see .NET and .NET Core Support Policy. 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;
}
}
<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:
- Calling StateHasChanged in developer code notifies a component that its state has changed and typically enqueues component rerendering to update the UI. StateHasChanged is covered in more detail later in ASP.NET Core Razor component lifecycle and ASP.NET Core Razor component rendering.
- The button's
@onclick
directive attribute attaches an event handler to the button'sonclick
event. Event handling is covered in more detail later in ASP.NET Core Blazor event handling.
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"
<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 ofInitiallyExpanded
to its initial value offalse
. - 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'sInitiallyExpanded
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 theOnInitialized
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;
}
}
<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 primary-subordinate 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);
}
}
<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"
<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:
- Binding with component parameters
- Bind across more than two components
- Blazor Two Way Binding Error (dotnet/aspnetcore #24599)
For more information on change detection, including information on the exact types that Blazor checks, see ASP.NET Core Razor component rendering.
ASP.NET Core