How do React hooks know which component instance is being called, without being passed a stable identifier?

There are many high quality answers online. For me, they either focus too much on React internals (Fibers, render cycle, ReactFiberHooks), or are too high level (“it’s just arrays”, closures, call order).

Here is an answer, written for and by me:

React stores state for a component instance by using its relative position in the “virtual DOM” tree. The Rules of Hooks requires hooks for a component to have a fixed order, so the call order can then be used to index an individual hook’s state.

This is alluded to in the React docs:

React keeps track of which state belongs to which component based on their place in the UI tree.

When you give a component state, you might think the state “lives” inside the component. But the state is actually held inside React. React associates each piece of state it’s holding with the correct component by where that component sits in the render tree.

Imagine keeping a second state tree that mirrors the shape of the UI tree. When we evaluate the UI tree, we walk through the UI tree and state tree at the same positions. Before a functional component is evaluated, its state is loaded from the corresponding state tree node to be used implicitly by its hooks.

Here is how you might implement a bare bones useState:

let context = {hookState: [], hookIndex: 0} // global hook "context"
// (unrelated to React Context API)

// 0. on first render, stateNode tree copies vnode (virtual DOM) tree structure
// visit() will be called per component instance/node when rendering 
function visit(vnode, stateNode) {
  // 1. before a functional component is called, load in its context
  context = stateNode
  context.hookIndex = 0 // reset for the current component
  renderStuff(vnode) // 2. evaluates the functional component, calling its hooks
  vnode.children.forEach((child, i) => visit(child, stateNode.children[i]))
}

// called by functional component
function useState(initial) {
  // 3. keep track of which hook is being called within the component
  const index = context.hookIndex // copy to closure
  context.hookIndex += 1

  // 4. if there is no prior state, this is the first render. initialize!
  if (index >= context.hookState.length) context.hookState[index] = initial

  return [
    context.hookState[index], // current value
    (value) => { // and setter
      context.hookState[index] = value
      queueRender()
    }
  ]
}

Conditionally rendered elements

How does this work for conditionally rendered elements? If an element is not rendered, then wouldn’t it lose its place in the rendering tree, messing up the rest of the tree?

My own confusion around this stemmed from the convenient representation of JSX.

JSX tags are transformed into function calls like this (try for yourself):

<Parent name="example">
  {showA && <A/>}
  <B/>
  <C/>
</Parent>
_jsxs(Parent, {
  name: "example",
  children: [
    showA && _jsx(A, {}), 
    _jsx(B, {}),
    _jsx(C, {})
  ]
});

Where _jsx and _jsxs are implemented by React or your framework of choice.

Notice that each of the children occupy their own index of an array passed into the parent _jsxs call.

More importantly, the entire conditional expression {showA && <A/>} takes its own place in the array. So the index of all the children in the “UI tree” stay the same, even when one child is not rendered. The element that isn’t rendered does lose its state, though.

For components that return different JSX return (showA ? <A/> : <B/>), React should reset the state subtree if the subtree’s node structure changes.

Keys and lists of elements

This also explains why lists of elements need a key. The entire array producing expression occupies an array index, so it becomes ambiguous which component is which since they share an index.

<Parent name="example">
  {arr.map(item => <A/>)}
  <B/>
  <C/>
</Parent>
_jsxs(Parent, {
  name: "example",
  children: [
    arr.map(item => _jsx(A, {})), 
    _jsx(B, {}),
    _jsx(C, {})
  ]
});

In summary1

  1. Each JSX tag is transformed into a _jsx function call, passing in the tag name as a string, properties as an object. A tag’s child tags are passed as an array of expressions.
  2. React executes this function call and constructs a “virtual DOM” tree describing the intended DOM structure. React knows how to efficiently update the real DOM from the virtual DOM.
  3. React stores local state for each component in a tree mirroring the virtual DOM. When state is updated, it uses the position within the tree to load the right state for hooks.
  4. Hooks within the same component are distinguished by call order in that component. This is possible because of the Rules of Hooks.
  5. This works with a conditionally rendered component (i.e. {showThing && <Thing/>}) because the entire conditional expression is included in the children array of its parent, so the index of the component is stable.
  6. Lists of components need keys because the entire list expression occupies a single index in the children array of its parent, so they need their own stable identifier.

Resources


  1. I like lists and bullets and I am not AI. Tautology.town is written by hand. I like em-dashes too, but that battle has been lost.