yewstack/yew · critical

unkeyed child in fully keyed list

Error message

unkeyed child in fully keyed list

What it means

During keyed-list diffing, Yew wraps existing child bundles in KeyedEntry, whose Borrow<Key>/Hash/Eq impls (packages/yew/src/dom_bundle/blist.rs:94) unwrap each bundle's key. The keyed code path (apply_keyed, selected at blist.rs:504 only when both old and new lists are flagged fully keyed) assumes every child carries a key; if a child that reached a fully keyed list has none, .expect panics with 'unkeyed child in fully keyed list'. The same rule React has: in a keyed list every child must be keyed on every render.

Source

Thrown at packages/yew/src/dom_bundle/blist.rs:94

    fn patch(self, node: VNode, bundle: &mut BNode) -> Self {
        test_log!("patching: {:?} -> {:?}", bundle, node);
        test_log!(
            "  parent={:?}, slot={:?}",
            self.parent.outer_html(),
            self.slot
        );
        // Advance the next sibling reference (from right to left)
        let next =
            node.reconcile_node(self.root, self.parent_scope, self.parent, self.slot, bundle);
        test_log!("  next_position: {:?}", next);
        Self { slot: next, ..self }
    }
}
/// Helper struct implementing [Eq] and [Hash] by only looking at a node's key
struct KeyedEntry(usize, BNode);
impl Borrow<Key> for KeyedEntry {
    fn borrow(&self) -> &Key {
        self.1.key().expect("unkeyed child in fully keyed list")
    }
}
impl Hash for KeyedEntry {
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
        <Self as Borrow<Key>>::borrow(self).hash(state)
    }
}
impl PartialEq for KeyedEntry {
    fn eq(&self, other: &Self) -> bool {
        <Self as Borrow<Key>>::borrow(self) == <Self as Borrow<Key>>::borrow(other)
    }
}
impl Eq for KeyedEntry {}

impl BNode {
    /// Assert that a bundle node is a list, or convert it to a list with a single child
    fn make_list(&mut self) -> &mut BList {
        match self {

View on GitHub (pinned to 0e4a05472f)

Solutions

  1. Give every child of that list a stable key, including static siblings: <li key={"footer"}>{"load more"}</li>
  2. If some children legitimately have no key, move them outside the keyed list into their own fragment or wrap them in a keyed element
  3. Ensure key={...} is unconditional — never switch a child between keyed and unkeyed across renders

Example fix

// before
html! {
    <ul>
        { for items.iter().map(|i| html! { <li key={i.id}>{ i.title.clone() }</li> }) }
        <li>{ "last updated just now" }</li>
    </ul>
}

// after
html! {
    <ul>
        { for items.iter().map(|i| html! { <li key={i.id}>{ i.title.clone() }</li> }) }
        <li key={"footer"}>{ "last updated just now" }</li>
    </ul>
}
Defensive patterns

Strategy: validation

Validate before calling

// debug helper: assert every child of a list is keyed before returning html
#[cfg(debug_assertions)]
fn assert_fully_keyed(children: &[VNode]) {
    for (i, c) in children.iter().enumerate() {
        assert!(c.key().is_some(), "child at index {i} is unkeyed inside a keyed list");
    }
}

Type guard

// returns true when every node carries a key — use to choose keyed vs unkeyed rendering
fn all_keyed(children: &[VNode]) -> bool { children.iter().all(|c| c.key().is_some()) }

Prevention

When it happens

Trigger: A list (fragment or iterator output) whose children are all keyed on one render, then a later render mixes in an unkeyed child (e.g. a static <li> appended next to keyed iterator items); conditionally dropping the key={...} prop on some children between renders; nested fragments where the outer list is treated as fully keyed but an inner node never got a key.

Common situations: Appending a footer/header element inside a keyed list fragment; refactoring a keyed iterator and leaving a placeholder element without a key; keys derived from Option (key={item.id} where id is sometimes None, producing an unkeyed child).

Related errors


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