Summary
key on a child element controls hook-scope identity but has no effect on DOM child matching, which is purely index-based. As a result, adding key to a reorderable list is actively harmful: when items reorder, hook state (useState/useRef/useEffect) follows the key while the DOM element at each position is patched from the new vnode at the same index — so imperatively-driven DOM (e.g. an icon painted by a useEffect in the key-scope) and index-patched props (e.g. onclick) desync.
Version
@tishlang/lattish 4.0.0.
Where in the source (dist/Lattish.js)
enterChildScope(f, keyOrNull) keys the hook scope by the key:
if (keyOrNull != null) { id = "k:" + String(keyOrNull); } // keyed scope
else { /* fnId + sibling-index */ }
- The child DOM reconcile loop matches purely by index — no key lookup:
let i = 0;
while (i < ncx.length) {
let o = ocx[i]; // old child at index i
let n = ncx[i]; // new child at index i
let childEl = activeHost.childAt(el, i); // DOM node at index i
...
}
So key moves the hook scope but not the DOM node. React (and most keyed vdom reconcilers) use key for both scope identity and DOM child matching; lattish uses it for scope only.
Repro (behavioral)
- Render a list of rows with
key={item.id}, where each row has (a) a useEffect that imperatively paints an icon into a ref, and (b) an onclick bound to item.
- Reorder the list (e.g. move the first item to the end) and re-render.
- Observed: the icon (driven by the key-scoped effect) and the
onclick/text (patched by index) no longer correspond to the same item — they desync.
- Removing
key (keyless, index-reconciled for both scope and DOM) is consistent again.
Impact
key is meant to make reordering correct, but here it makes reordering incorrect relative to the keyless case, which is surprising and the opposite of the React contract. Practical guidance today is "never key a reorderable list," which defeats the purpose of keys.
Expected
Either (a) make child DOM reconciliation key-aware (match old/new children by key, moving DOM nodes to follow their keyed scope), or (b) if index-based DOM matching is intentional, document that key only affects hook-scope identity and must not be used on reorderable lists.
Summary
keyon a child element controls hook-scope identity but has no effect on DOM child matching, which is purely index-based. As a result, addingkeyto a reorderable list is actively harmful: when items reorder, hook state (useState/useRef/useEffect) follows the key while the DOM element at each position is patched from the new vnode at the same index — so imperatively-driven DOM (e.g. an icon painted by auseEffectin the key-scope) and index-patched props (e.g.onclick) desync.Version
@tishlang/lattish4.0.0.Where in the source (dist/Lattish.js)
enterChildScope(f, keyOrNull)keys the hook scope by the key:So
keymoves the hook scope but not the DOM node. React (and most keyed vdom reconcilers) usekeyfor both scope identity and DOM child matching; lattish uses it for scope only.Repro (behavioral)
key={item.id}, where each row has (a) auseEffectthat imperatively paints an icon into aref, and (b) anonclickbound toitem.onclick/text (patched by index) no longer correspond to the same item — they desync.key(keyless, index-reconciled for both scope and DOM) is consistent again.Impact
keyis meant to make reordering correct, but here it makes reordering incorrect relative to the keyless case, which is surprising and the opposite of the React contract. Practical guidance today is "neverkeya reorderable list," which defeats the purpose of keys.Expected
Either (a) make child DOM reconciliation key-aware (match old/new children by key, moving DOM nodes to follow their keyed scope), or (b) if index-based DOM matching is intentional, document that
keyonly affects hook-scope identity and must not be used on reorderable lists.