React path · State and hooks · Topic 5 of 12

Hooks, call order and closures

Not started

A function component is called again on every render, so its local variables are created from scratch each time. Hooks are how it remembers anything: useState, useRef, useEffect and the rest ask React for data it stored for this component instance. React stores that data in a list and matches calls to entries by order, which is where the rules of hooks come from. And because every render is a separate function call, every render also creates its own set of closures.

Why it matters: The rules of hooks look arbitrary until you see how React stores hook state: by the order of the calls. And stale values in callbacks are ordinary JavaScript closures, one set per render.

  1. 1State is stored by call order

    React keeps a list of hook slots for each component instance and hands them out in the order the hooks are called. There are no names, only positions.

  2. 2So call them the same way every time

    Only call hooks at the top level of a component or custom hook, never in conditions, loops or after an early return, so the order can't change between renders.

  3. 3Every render makes new closures

    Handlers and effects created during a render close over that render's props and state. A callback kept from an earlier render keeps seeing the earlier values.

1

How it works

The explanation, with examples you can run.

Slots in call order

Here is a toy version of useState (not React's code, but the same idea), with a component that breaks the rule:

⌘/Ctrl + Enter
OutputNot run yet

Press Run to see what your code prints, in order.

Runs in this browser, in an isolated frame with no access to this page. console.log, info, warn and error are shown; code runs as a classic script, so use an async function for await.

The first render fills three slots: "Ada", 36, "dark". In the second, the condition skips a call, so theme gets slot 1 and reads 36. React works the same way: hooks have no names, only positions. That is why hooks must run at the top level, unconditionally, in the same order every render. React's lint rule (eslint-plugin-react-hooks) catches violations, and in development React warns when the order changes.

The same reasoning explains the other rule: hooks may only be called while React is rendering a component (or from a custom hook it calls), because only then does React know whose slots to use.

One set of closures per render

⌘/Ctrl + Enter
OutputNot run yet

Press Run to see what your code prints, in order.

Runs in this browser, in an isolated frame with no access to this page. console.log, info, warn and error are shown; code runs as a classic script, so use an async function for await.

Each call made its own count and its own arrow function that closed over it, so the three handlers return [0, 1, 2]. In React that is usually what you want: a click handler from the render on screen sees the state on screen.

It becomes a bug when a callback from an old render outlives it:

import { useEffect, useState } from "react" export function Ticker() {  const [count, setCount] = useState(0)   useEffect(() => {    const id = setInterval(() => {      setCount(count + 1) // always 0 + 1: this closure is from the first render    }, 1000)    return () => clearInterval(id)  }, []) // the effect never re-runs, so the closure is never replaced   return <p>{count}</p>}

The counter goes to 1 and stays there. Fixes, from simplest: use an updater (setCount((c) => c + 1)), which doesn't need count at all; or list count in the dependencies so the effect is recreated with each new value; or read the latest value from a ref.

Custom hooks

A function whose name starts with use and that calls other hooks is a custom hook. It is not a new mechanism: its hook calls simply take their slots in the calling component's list, in order. Two components using the same custom hook get independent state.

2

Check yourself

3 questions. Get them all right to finish this topic.

  1. 1.A component calls `useState` inside `if (showDetails)`. When `showDetails` flips to false, what goes wrong?
  2. 2.An effect with `[]` dependencies starts `setInterval(() => console.log(count), 1000)`. You click to increase `count`. What is logged?
  3. 3.Where may you call a hook?

0 of 3 right so far. Get all three to finish this topic.