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
- Give every child of that list a stable key, including static siblings: <li key={"footer"}>{"load more"}</li>
- If some children legitimately have no key, move them outside the keyed list into their own fragment or wrap them in a keyed element
- 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
- Key every child of a list including static siblings like footers or separators
- Never make the key prop conditional on data — Option keys create unkeyed children
- Keep keyed and unkeyed children in separate fragments/lists
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
- invalid attribute key
- could not set property
- could not remove attribute
- could not remove property
- can't create namespaced element for vtag
AI-assisted analysis of yewstack/yew@0e4a05472f (2026-08-22).
Data as JSON: /api/errors/8f25ae50cc7f5ec2.
Report an issue: GitHub.