yewstack/yew · error · syn::Error
duplicate key for a node in a `{loop_kind}`-loop this will c
Error message
duplicate key for a node in a `{loop_kind}`-loop
this will create elements with duplicate keys if the loop iterates more than once What it means
`parse_loop_body` (shared by `for` and `while` in `html!`) scans each top-level element child that carries a `key` prop and rejects keys that are `is_contextless_pure` — string/int literals or multi-segment paths that cannot reference the loop variable (html_loop.rs:12-18, 40-48). Such a key evaluates identically on every iteration, which would create duplicate keys and break Yew's keyed `VList` diffing, so it is caught at compile time.
Source
Thrown at packages/yew-macro/src/html_tree/html_loop.rs:41
body_stream: ParseStream,
loop_kind: &str,
) -> syn::Result<(Vec<Stmt>, HtmlChildrenTree, TokenStream)> {
let stmts = parse_preamble_stmts(body_stream)?;
let body = HtmlChildrenTree::parse_delimited_with_nodes(body_stream)?;
let deprecations = super::check_unnecessary_fragment(&body);
// TODO: more concise code by using if-let guards (MSRV 1.95)
for child in body.0.iter() {
let HtmlTree::Element(element) = child else {
continue;
};
let Some(key) = &element.props.special.key else {
continue;
};
if is_contextless_pure(&key.value) {
return Err(syn::Error::new(
key.value.span(),
format!(
"duplicate key for a node in a `{loop_kind}`-loop\nthis will create elements \
with duplicate keys if the loop iterates more than once"
),
));
}
}
Ok((stmts, body, deprecations))
}
/// Emit a loop that accumulates its body children into a `VList`.
///
/// `loop_header` is the native Rust loop syntax without its body, e.g.
/// `for #pat in #iter` or `while #cond`.
pub(super) fn emit_loop(
loop_header: TokenStream,View on GitHub (pinned to 0e4a05472f)
Solutions
- Derive the key from per-item unique data: `key={item.id}`
- If items have no natural id, enumerate the iterator and key on the index: `for (i, item) in items.iter().enumerate() { … key={i} }`
- Remove the key entirely so the list diffs by position — only safe for append-only, never-reordered lists
Example fix
// before
html! { for item in items { <li key="row">{ item.name }</li> } }
// after
html! { for (i, item) in items.iter().enumerate() { <li key={i}>{ item.name }</li> } } Defensive patterns
Strategy: validation
Validate before calling
// Convention check for review: a loop body's key expression should mention
// the loop binding (item) or an enumerate index — never a bare literal:
// for item in items { <li key={item.id}>…</li> } // good
// for (i, item) in items.iter().enumerate() { <li key={i}>…</li> } // good
// for item in items { <li key="row">…</li> } // rejected by the macro Prevention
- Make keyed lists derive keys from a stable per-item id (`key={item.id}`)
- Default to `.enumerate()` and `key={i}` when items lack ids
- Never paste a static key into markup that is about to become a loop — the macro will (correctly) reject it
When it happens
Trigger: `html! { for item in items { <li key="row">{ item }</li> } }` (literal key) or `key={consts::ROW_KEY}` (multi-segment constant path). Keys referencing the loop binding, like `key={item.id}` or a single identifier, are not flagged.
Common situations: Prototyping with one item and a hardcoded key, copy-pasting a static key down a list of similar elements, or converting hand-written repeated markup into a loop without re-keying it.
Related errors
- string literals must not contain more than one class (hint:
- expected a valid closing tag for component note: found openi
- missing condition for `if` expression
- this `if` expression has a condition, but no block
- expected block or `if` after `else`
AI-assisted analysis of yewstack/yew@0e4a05472f (2026-08-22).
Data as JSON: /api/errors/52d1463da9eb5855.
Report an issue: GitHub.