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

  1. Derive the key from per-item unique data: `key={item.id}`
  2. If items have no natural id, enumerate the iterator and key on the index: `for (i, item) in items.iter().enumerate() { … key={i} }`
  3. 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

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


AI-assisted analysis of yewstack/yew@0e4a05472f (2026-08-22). Data as JSON: /api/errors/52d1463da9eb5855. Report an issue: GitHub.