Hooks

Published:

React component is still a function:

function Counter() {
  // ...
}

React can call that function, let it return, and call it again later. There is no component object in the function body where count can remain between those calls.

That leaves the next hole:

if function components have no instance object, where can stateful behavior attach?

We have already used useState. This time we will treat it as evidence and reconstruct the more general idea behind it.

An ordinary call #

useState is a JavaScript function, so perhaps it can create remembered state wherever we call it.

Try the smallest possible call outside a component:

const { useState } = React;

const [count] = useState(0);
console.log('count:', count);

Waiting to run

Not run yet.

The call fails. React reports an invalid Hook call because no component is currently being rendered.

That gives us the first constraint:

a Hook call needs an active React render to attach to

The same function call works inside a component that React renders:

const { useState } = React;

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(count + 1)}>
      Count: {count}
    </button>
  );
}

root.render(<Counter />);

Waiting to run

Not run yet.

The syntax of the call did not change. Its surrounding execution did.

When React renders <Counter />, React knows which component in the tree it is currently calculating. useState can use that render context to reach state associated with that component identity.[5]

So useState does not put the value inside the Counter function. It lets the current Counter render reach a value React keeps outside the function.[4]

More than one attachment #

Component identity identifies which component owns the state. It is not enough to identify which piece of state a Hook call wants.

A component may call useState more than once:

const { useState } = React;

function Scoreboard() {
  const [home, setHome] = useState(0);
  const [away, setAway] = useState(0);

  return (
    <div>
      <button onClick={() => setHome(home + 1)}>
        Home: {home}
      </button>
      <button onClick={() => setAway(away + 1)}>
        Away: {away}
      </button>
    </div>
  );
}

root.render(<Scoreboard />);

setTimeout(() => {
  const buttons = mountNode.querySelectorAll('button');
  buttons[1].click();

  setTimeout(() => {
    console.log(buttons[0].textContent);
    console.log(buttons[1].textContent);
  }, 0);
}, 20);

Waiting to run

Not run yet.

Both calls belong to the same rendered Scoreboard, yet their values remain independent.

There is no name passed to useState:

useState(0); // no "home" name
useState(0); // no "away" name

The local names appear only after each call returns. JavaScript destructuring names the returned values for our code; it cannot tell React which stored value the call requested.

Something else has to distinguish the two calls.

Call position #

The visible distinction is their position in the component's sequence of Hook calls:

function Scoreboard() {
  const [home, setHome] = useState(0); // first Hook call
  const [away, setAway] = useState(0); // second Hook call
  // ...
}

An intentionally simplified model would look like this:

function renderComponent(componentIdentity) {
  currentComponent = componentIdentity;
  currentHookIndex = 0;

  return componentIdentity.type(componentIdentity.props);
}

function useState(initialValue) {
  const slot = currentHookIndex;
  currentHookIndex += 1;

  return stateFor(currentComponent, slot, initialValue);
}

At the start of a render, the Hook index returns to zero. The first call reaches the first slot, the second call reaches the second slot, and so on. The stored values belong to the rendered component identity.[a]

This is the smallest model that explains what we have observed:

  • the same component function can have separate state at separate tree positions
  • one rendered component can have several independent state values
  • the initial value is only needed when a slot is created[2]
  • later renders can reach the same slots even though the function's local variables are new

The model makes a prediction. If the order of Hook calls changes between renders, the address of every later slot changes with it.

A conditional call #

Put a Hook inside a condition so that one render calls two Hooks and another render calls three:

const { useState } = React;

function Profile() {
  const [expanded, setExpanded] = useState(false);

  if (expanded) {
    const [note] = useState('Private note');
    console.log('note:', note);
  }

  const [theme] = useState('dark');

  return (
    <button onClick={() => setExpanded(true)}>
      {expanded ? 'Expanded' : 'Expand'} · {theme}
    </button>
  );
}

root.render(<Profile />);

setTimeout(() => {
  mountNode.querySelector('button').click();
}, 20);

Waiting to run

Not run yet.

On the first render, the sequence is:

  1. expanded
  2. theme

After the click, the sequence would become:

  1. expanded
  2. note
  3. theme

The second Hook position used to mean theme and now means note. React cannot preserve both meanings for the same slot, so it reports that the order of Hooks changed and rejects the render.

The runtime error exposes the constraint behind the first Rule of Hooks:

call Hooks at the top level, not inside conditions, loops, nested functions, or after a conditional early return[3][b]

"Top level" here means the top level of the component or of another Hook. It does not mean the top level of the JavaScript module.

Keep the call, condition the result #

The component still needs optional details. The condition belongs in the rendered description, while the Hook call stays in the stable sequence:

const { useState } = React;

function Profile() {
  const [expanded, setExpanded] = useState(false);
  const [note, setNote] = useState('Private note');
  const [theme] = useState('dark');

  return (
    <section>
      <button onClick={() => setExpanded(!expanded)}>
        {expanded ? 'Collapse' : 'Expand'} · {theme}
      </button>
      {expanded && (
        <label>
          Note
          <input
            value={note}
            onChange={(event) => setNote(event.currentTarget.value)}
          />
        </label>
      )}
    </section>
  );
}

root.render(<Profile />);

setTimeout(() => {
  const button = mountNode.querySelector('button');
  button.click();

  setTimeout(() => {
    console.log('details visible:', Boolean(mountNode.querySelector('input')));
    button.click();
  }, 0);
}, 20);

Waiting to run

Not run yet.

Every render now calls the same three Hooks in the same order. expanded only decides whether the input element appears in the returned tree.

This separation is important:

  • Hook calls declare which React features this component uses
  • ordinary JavaScript decides which description the current values produce

The UI can branch. The Hook sequence stays stable.

React functions only #

Stable order explains where Hooks must appear inside a component. We still need a rule for which functions may contain them.

React supports two places:[3]

  1. a function component that React is rendering
  2. another Hook called as part of that render

An ordinary event handler runs later, after rendering has finished. There is no active render for a Hook to attach to:

const { useState } = React;

function Counter() {
  function handleClick() {
    const [count] = useState(0);
    console.log('count:', count);
  }

  return <button onClick={handleClick}>Read state</button>;
}

root.render(<Counter />);

setTimeout(() => {
  mountNode.querySelector('button').click();
}, 20);

Waiting to run

Not run yet.

The handler is nested inside the component in source code, but it is not called while React renders the component. Lexical nesting does not supply render context.

Move the Hook call into the component. The later handler closes over the value and setter returned during that render:

function Counter() {
  const [count, setCount] = useState(0);

  function handleClick() {
    setCount(count + 1);
  }

  return <button onClick={handleClick}>Count: {count}</button>;
}

The second allowed place—another Hook—is how React logic can eventually be composed and reused. By convention, Hook names begin with use, which makes the special call visible to readers and to React's lint rules. A later article will build a custom Hook rather than hiding that separate composition problem here.

Hooks are a family #

useState is only the first member we needed. Hooks are functions that let a rendered component use React features.[1]

Different Hooks connect a component to different kinds of React-managed behavior:

  • state Hooks remember values that affect rendering
  • context Hooks read values supplied by distant ancestors
  • ref Hooks retain values that do not trigger rendering
  • effect Hooks synchronize with systems outside React
  • performance Hooks let React reuse or defer work

They do not all store an ordinary state value, but they share the same component render context and the same call-order constraint.

This is why "Hook" describes more than a naming style. Calling a function useSomething does not create new runtime powers. The function becomes a custom Hook when it participates in this protocol by calling other Hooks and following their rules.

Filling the hole #

A function component does not need an instance object in application code. React already has the missing identity: the component type, position, and key established during reconciliation.

When React calls that component, it establishes a render context. Hook calls use that context plus their stable call positions to reach React-managed slots associated with the rendered component.

That fills the hole:

  1. reconciliation identifies the rendered component
  2. React begins rendering that component and resets its Hook position
  3. each Hook call reaches the next React-managed slot
  4. the component returns its next UI description
  5. later updates cause another render, whose Hook calls reach the same slots in the same order

The Rules of Hooks preserve the address system. They are structural requirements of the model, not arbitrary formatting preferences.

Final definition #

A Hook is a function that lets a component use React-managed features during rendering. React associates Hook data with a rendered component identity, and stable top-level call order lets each render reach the same Hook slots.

Hooks must therefore be called while React is rendering a function component or another Hook, and ordinary Hooks must be called in the same order on every render.

Summary #

Hooks fill the instance hole left by function components:

  • state does not live in the component function's local variables
  • React stores Hook data against a rendered component identity
  • a Hook call needs an active React render context
  • call position distinguishes multiple Hooks used by one component
  • conditional calls change those positions and break the mapping
  • Hooks therefore belong at the top level of components or other Hooks
  • event handlers may use values and setters returned by Hooks, but may not call Hooks themselves
  • useState is one Hook in a larger family of React features

Notes

  1. The live demos in this article use the React 19.2.5 and react-dom 19.2.5 development modules. The slot model developed here is an author-facing explanation of observable behavior, not a reproduction of React's internal data structures. · Back
  2. React's `use` API is a special exception to the usual top-level rule: it may be called in loops and conditions, though it still has other restrictions. This article develops the rule followed by Hooks such as `useState`, `useReducer`, `useContext`, and `useEffect`. · Back

References

  1. React: Built-in React Hooks (opens in a new tab) · Back
  2. React `useState` API reference (opens in a new tab) · Back
  3. React: Rules of Hooks (opens in a new tab) · Back
  4. React: State: A Component's Memory (opens in a new tab) · Back
  5. React: React calls Components and Hooks (opens in a new tab) · Back