Retain element, component, and model relationships 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.
This article explains how to use the @key
directive attribute to retain element, component, and model relationships when rendering and the elements or components subsequently change.
Use of the @key
directive attribute
When rendering a list of elements or components and the elements or components subsequently change, Blazor must decide which of the previous elements or components are retained and how model objects should map to them. Normally, this process is automatic and sufficient for general rendering, but there are often cases where controlling the process using the @key
directive attribute is required.
Consider the following example that demonstrates a collection mapping problem that's solved by using @key
.
For the following components:
- The
Details
component receives data (Data
) from the parent component, which is displayed in an<input>
element. Any given displayed<input>
element can receive the focus of the page from the user when they select one of the<input>
elements. - The parent component creates a list of person objects for display using the
Details
component. Every three seconds, a new person is added to the collection.
This demonstration allows you to:
- Select an
<input>
from among several renderedDetails
components. - Study the behavior of the page's focus as the people collection automatically grows.
Details.razor
:
<input value="@Data" />
@code {
[Parameter]
public string? Data { get; set; }
}
<input value="@Data" />
@code {
[Parameter]
public string? Data { get; set; }
}
<input value="@Data" />
@code {
[Parameter]
public string? Data { get; set; }
}
<input value="@Data" />
@code {
[Parameter]
public string? Data { get; set; }
}
<input value="@Data" />
@code {
[Parameter]
public string Data { get; set; }
}
<input value="@Data" />
@code {
[Parameter]
public string Data { get; set; }
}
In the following parent component, each iteration of adding a person in OnTimerCallback
results in Blazor rebuilding the entire collection. The page's focus remains on the same index position of <input>
elements, so the focus shifts each time a person is added. Shifting the focus away from what the user selected isn't desirable behavior. After demonstrating the poor behavior with the following component, the @key
directive attribute is used to improve the user's experience.
People.razor
:
@page "/people"
@using System.Timers
@implements IDisposable
<PageTitle>People</PageTitle>
<h1>People Example</h1>
@foreach (var person in people)
{
<Details Data="@person.Data" />
}
@code {
private Timer timer = new Timer(3000);
public List<Person> people =
new()
{
{ new Person { Data = "Person 1" } },
{ new Person { Data = "Person 2" } },
{ new Person { Data = "Person 3" } }
};
protected override void OnInitialized()
{
timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
timer.Start();
}
private void OnTimerCallback()
{
_ = InvokeAsync(() =>
{
people.Insert(0,
new Person
{
Data = $"INSERTED {DateTime.Now.ToString("hh:mm:ss tt")}"
});
StateHasChanged();
});
}
public void Dispose() => timer.Dispose();
public class Person
{
public string? Data { get; set; }
}
}
People.razor
:
@page "/people"
@using System.Timers
@implements IDisposable
<PageTitle>People</PageTitle>
<h1>People Example</h1>
@foreach (var person in people)
{
<Details Data="@person.Data" />
}
@code {
private Timer timer = new Timer(3000);
public List<Person> people =
new()
{
{ new Person { Data = "Person 1" } },
{ new Person { Data = "Person 2" } },
{ new Person { Data = "Person 3" } }
};
protected override void OnInitialized()
{
timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
timer.Start();
}
private void OnTimerCallback()
{
_ = InvokeAsync(() =>
{
people.Insert(0,
new Person
{
Data = $"INSERTED {DateTime.Now.ToString("hh:mm:ss tt")}"
});
StateHasChanged();
});
}
public void Dispose() => timer.Dispose();
public class Person
{
public string? Data { get; set; }
}
}
PeopleExample.razor
:
@page "/people-example"
@using System.Timers
@implements IDisposable
@foreach (var person in people)
{
<Details Data="person.Data" />
}
@code {
private Timer timer = new Timer(3000);
public List<Person> people =
new()
{
{ new Person { Data = "Person 1" } },
{ new Person { Data = "Person 2" } },
{ new Person { Data = "Person 3" } }
};
protected override void OnInitialized()
{
timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
timer.Start();
}
private void OnTimerCallback()
{
_ = InvokeAsync(() =>
{
people.Insert(0,
new Person
{
Data = $"INSERTED {DateTime.Now.ToString("hh:mm:ss tt")}"
});
StateHasChanged();
});
}
public void Dispose() => timer.Dispose();
public class Person
{
public string? Data { get; set; }
}
}
PeopleExample.razor
:
@page "/people-example"
@using System.Timers
@implements IDisposable
@foreach (var person in people)
{
<Details Data="person.Data" />
}
@code {
private Timer timer = new Timer(3000);
public List<Person> people =
new()
{
{ new Person { Data = "Person 1" } },
{ new Person { Data = "Person 2" } },
{ new Person { Data = "Person 3" } }
};
protected override void OnInitialized()
{
timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
timer.Start();
}
private void OnTimerCallback()
{
_ = InvokeAsync(() =>
{
people.Insert(0,
new Person
{
Data = $"INSERTED {DateTime.Now.ToString("hh:mm:ss tt")}"
});
StateHasChanged();
});
}
public void Dispose() => timer.Dispose();
public class Person
{
public string? Data { get; set; }
}
}
PeopleExample.razor
:
@page "/people-example"
@using System.Timers
@implements IDisposable
@foreach (var person in people)
{
<Details Data="person.Data" />
}
@code {
private Timer timer = new Timer(3000);
public List<Person> people =
new()
{
{ new Person { Data = "Person 1" } },
{ new Person { Data = "Person 2" } },
{ new Person { Data = "Person 3" } }
};
protected override void OnInitialized()
{
timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
timer.Start();
}
private void OnTimerCallback()
{
_ = InvokeAsync(() =>
{
people.Insert(0,
new Person
{
Data = $"INSERTED {DateTime.Now.ToString("hh:mm:ss tt")}"
});
StateHasChanged();
});
}
public void Dispose() => timer.Dispose();
public class Person
{
public string Data { get; set; }
}
}
PeopleExample.razor
:
@page "/people-example"
@using System.Timers
@implements IDisposable
@foreach (var person in people)
{
<Details Data="person.Data" />
}
@code {
private Timer timer = new Timer(3000);
public List<Person> people =
new List<Person>()
{
{ new Person { Data = "Person 1" } },
{ new Person { Data = "Person 2" } },
{ new Person { Data = "Person 3" } }
};
protected override void OnInitialized()
{
timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
timer.Start();
}
private void OnTimerCallback()
{
_ = InvokeAsync(() =>
{
people.Insert(0,
new Person
{
Data = $"INSERTED {DateTime.Now.ToString("hh:mm:ss tt")}"
});
StateHasChanged();
});
}
public void Dispose() => timer.Dispose();
public class Person
{
public string Data { get; set; }
}
}
The contents of the people
collection changes with inserted, deleted, or re-ordered entries. Rerendering can lead to visible behavior differences. For example, each time a person is inserted into the people
collection, the user's focus is lost.
The mapping process of elements or components to a collection can be controlled with the @key
directive attribute. Use of @key
guarantees the preservation of elements or components based on the key's value. If the Details
component in the preceding example is keyed on the person
item, Blazor ignores rerendering Details
components that haven't changed.
To modify the parent component to use the @key
directive attribute with the people
collection, update the <Details>
element to the following:
<Details @key="person" Data="@person.Data" />
When the people
collection changes, the association between Details
instances and person
instances is retained. When a Person
is inserted at the beginning of the collection, one new Details
instance is inserted at that corresponding position. Other instances are left unchanged. Therefore, the user's focus isn't lost as people are added to the collection.
Other collection updates exhibit the same behavior when the @key
directive attribute is used:
- If an instance is deleted from the collection, only the corresponding component instance is removed from the UI. Other instances are left unchanged.
- If collection entries are re-ordered, the corresponding component instances are preserved and re-ordered in the UI.
Important
Keys are local to each container element or component. Keys aren't compared globally across the document.
When to use @key
Typically, it makes sense to use @key
whenever a list is rendered (for example, in a foreach
block) and a suitable value exists to define the @key
.
You can also use @key
to preserve an element or component subtree when an object doesn't change, as the following examples show.
Example 1:
<li @key="person">
<input value="@person.Data" />
</li>
Example 2:
<div @key="person">
@* other HTML elements *@
</div>
If an person
instance changes, the @key
attribute directive forces Blazor to:
- Discard the entire
<li>
or<div>
and their descendants. - Rebuild the subtree within the UI with new elements and components.
This is useful to guarantee that no UI state is preserved when the collection changes within a subtree.
Scope of @key
The @key
attribute directive is scoped to its own siblings within its parent.
Consider the following example. The first
and second
keys are compared against each other within the same scope of the outer <div>
element:
<div>
<div @key="first">...</div>
<div @key="second">...</div>
</div>
The following example demonstrates first
and second
keys in their own scopes, unrelated to each other and without influence on each other. Each @key
scope only applies to its parent <div>
element, not across the parent <div>
elements:
<div>
<div @key="first">...</div>
</div>
<div>
<div @key="second">...</div>
</div>
For the Details
component shown earlier, the following examples render person
data within the same @key
scope and demonstrate typical use cases for @key
:
<div>
@foreach (var person in people)
{
<Details @key="person" Data="@person.Data" />
}
</div>
@foreach (var person in people)
{
<div @key="person">
<Details Data="@person.Data" />
</div>
}
<ol>
@foreach (var person in people)
{
<li @key="person">
<Details Data="@person.Data" />
</li>
}
</ol>
The following examples only scope @key
to the <div>
or <li>
element that surrounds each Details
component instance. Therefore, person
data for each member of the people
collection is not keyed on each person
instance across the rendered Details
components. Avoid the following patterns when using @key
:
@foreach (var person in people)
{
<div>
<Details @key="person" Data="@person.Data" />
</div>
}
<ol>
@foreach (var person in people)
{
<li>
<Details @key="person" Data="@person.Data" />
</li>
}
</ol>
When not to use @key
There's a performance cost when rendering with @key
. The performance cost isn't large, but only specify @key
if preserving the element or component benefits the app.
Even if @key
isn't used, Blazor preserves child element and component instances as much as possible. The only advantage to using @key
is control over how model instances are mapped to the preserved component instances, instead of Blazor selecting the mapping.
Values to use for @key
Generally, it makes sense to supply one of the following values for @key
:
- Model object instances. For example, the
Person
instance (person
) was used in the earlier example. This ensures preservation based on object reference equality. - Unique identifiers. For example, unique identifiers can be based on primary key values of type
int
,string
, orGuid
.
Ensure that values used for @key
don't clash. If clashing values are detected within the same parent element, Blazor throws an exception because it can't deterministically map old elements or components to new elements or components. Only use distinct values, such as object instances or primary key values.