Blazor @key: Keeping Component State Attached to the Right Data
When list items move or disappear, Blazor's default position-based diffing can reuse component instances and leak local state into the wrong row. The `@key` directive fixes this by binding identity to a stable data key.
26 Jul 2026, 16:16 UTC

The row that remembers the wrong note
You build a list of editable rows in Blazor. A user types a half-finished note into the second row, then deletes the first row. Instead of the second row keeping its own draft, the third row suddenly displays the text that belonged to the second. The data moved, but the component’s local state stayed behind.
The fix is often a single directive: @key. It tells Blazor’s renderer to match components by a stable data identifier rather than by list position, so state follows the item, not the slot.
Why position-based matching leaks state
Blazor renders components into a tree, then compares the previous tree to the new one to decide what to create, update, or destroy. By default, that diffing algorithm matches elements by position: the first child in the old list corresponds to the first child in the new list, the second to the second, and so on. This is efficient for static content, but it assumes order is stable.
When you remove, insert, or reorder items, positions shift. The renderer sees “the component at index 1 changed its parameters” and reuses the existing instance. Any local fields—an input draft, a toggle flag, focus state—remain attached to that instance. The result is state contamination: row three inherits row two’s UI because they occupied the same position across renders.
How @key overrides the default
The @key directive assigns an explicit identity to a component or element inside a loop. When the renderer encounters a keyed subtree, it matches by that key value instead of by ordinal position. If the key disappears, the subtree is discarded. If a new key appears, a fresh instance is created. If a key moves, the existing instance moves with it.
Keys must be stable identifiers from your data model—typically a database primary key or a GUID. A loop index is ephemeral: inserting an item at the top shifts every index by one, which recreates the same problem @key was meant to solve. Because the directive operates in the shared Razor Components rendering pipeline, it behaves identically in Blazor Server and Blazor WebAssembly; the hosting model only changes transport and execution, not the diffing rules.
Worked example: editable rows that keep their own drafts
Consider a parent component that renders a list of items. Each item has an Id and a Note, but the child component holds a local draft string that the user edits directly.
First, the child component. It accepts the item as a parameter but intentionally keeps its own local editing state:
<input @bind="draft" placeholder="Type a note..." />
@code {
[Parameter]
public RowItem Item { get; set; } = default!;
private string draft = string.Empty;
}
Next, the parent. Without a key, the loop looks like this:
@foreach (var row in rows)
{
<EditableRow Item="row" />
}
Add a button that removes the first item. After the removal, the component instance that previously displayed the second row is reused for the third row. Because draft is local and never reset, the third row shows the second row’s unsaved text.
Now add @key bound to the stable identifier:
@foreach (var row in rows)
{
<EditableRow @key="row.Id" Item="row" />
}
With the key in place, the renderer destroys the component whose key no longer exists and preserves the instances whose keys remain. The remaining rows keep their own drafts.
What to check in the browser
Open your browser’s developer tools and inspect the DOM. Before the fix, removing the first row leaves the same <input> element in place for the shifted data. After adding @key, the removed row’s input disappears and the surviving inputs stay attached to their original component instances. If the element identity follows the data, the directive is working.
Trade-offs and limitations
@key is not free. Applying it to every element in a large list adds comparison overhead, and the renderer may discard and recreate DOM nodes that could otherwise be updated in place. Reserve it for boundaries where identity actually matters—typically list items that own local state.
Keys must be unique within their parent container. Duplicate values cause rendering exceptions or unpredictable matching, so ensure your data source enforces uniqueness.
If your data model lacks a natural stable key, introducing one is a schema or DTO change that ripples through API contracts, serialization, and offline caches. That cost is real, but it is usually cheaper than debugging state that appears to teleport between rows.
Finally, @key controls component identity, not CSS or DOM identity. Styling or animations that depend on element order still require separate handling, such as CSS transition groups or a dedicated animation library.
Actionable takeaway
The next time a Blazor list behaves as though items swapped their input values, focus, or expanded state, inspect the loop. If the items have a stable identifier, add @key bound to that identifier. Then verify by removing or shuffling an item and confirming that local state stays with the data, not the position.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.