Skip to content

key scopes hooks by key but children reconcile by index — keyed reorderable lists desync DOM vs hook state #14

Description

@spacedevin

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)

  1. 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.
  2. Reorder the list (e.g. move the first item to the end) and re-render.
  3. 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.
  4. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions