Skip to content

Step 1 of 5 · Reading · ~4 min

Read

Hooks & State

The useState Hook

A hook gives a plain function component something that survives between renders. After this lesson your program runs a component body against an instance that owns a list of state slots and a cursor, and processes queued updates the way a hook runtime does.

Why state cannot be a local variable

The component function runs again from the top on every render, so any variable declared inside it starts over. State therefore lives outside the function, in a slot list owned by the component instance. A useState call is a request to that list.

render 1   useState("Ada")   slot 0 does not exist yet: create it with "Ada", return "Ada"
           useState(0)       slot 1: create it with 0, return 0
render 2   useState("Ada")   slot 0 exists: ignore the argument, return its current value
           useState(0)       slot 1: ignore the argument, return its current value

Before every render the cursor is rewound to 0, and each hook call takes the slot the cursor points at and advances it. The initial value is used only when the slot is created, on the first render. After that it is ignored, so the argument never resets anything.

Why the rules of hooks exist

A hook has no name and no identity. The first call of a render gets slot 0, the second gets slot 1, and so on. That is the whole mechanism, and it is why hooks may not sit inside an if, a loop or after an early return: skip one on some renders and every later hook shifts down a slot, so age reads the value that was stored for name.

A runtime can notice part of this. When a render makes a different number of calls than the first render did, React throws one of two errors, which your program will print:

  • Rendered more hooks than during the previous render.
  • Rendered fewer hooks than expected. This may be caused by an accidental early return statement.

It cannot notice a swap of two hooks, so that bug is silent.

Updates are queued, not applied

A setter does not change the variable your render already holds. It adds an update to the slot's queue, and the framework schedules a render. During that render the queue is processed in order. Two kinds of update exist:

UpdateMeaning
a valuereplace the slot with it
a function of the previous statethe slot becomes that function's result

Order matters. From 5, "add 1" then "set 5" ends at 5; "set 5" then "add 1" ends at 6. Three updates that each say "the captured count plus 1" all use the same captured number and add 1 in total; three updates of the function kind add 3, because each receives the result of the one before. That is why setCount(c => c + 1) is the form that composes under batching.

If the final value of every slot equals what it was before, the framework bails out: no render happens. Values are compared with Object.is, so two updates that cancel each other change nothing. A parent re-rendering is different: it always runs the body again, and it still drains the queues first.

Beyond useState

Passing a function to useState makes it a lazy initialiser, run once when the slot is created. useReducer stores a state and a reducer in the same kind of slot and fits better when transitions get interesting. useContext reads a value from the nearest provider above, which skips threading a prop through layers that do not care; it delivers a value, it does not store one.

The script you will run

MOUNT 0,Ada      first render: two useState calls, with these initial values
SET 1 Bob        queue a replacement for slot 1
ADD 0 1          queue "previous + 1" for slot 0
FLUSH            apply the queues; render, or bail out if nothing changed
RENDER 9,Zed     the parent renders the component again, calling useState with these values

Common mistakes

  • Using the initial value on every render, which resets state each time.
  • Forgetting to rewind the cursor, so the second render reads slots past the end.
  • Applying an update the moment it is queued, losing the order of mixed updates.
  • Rendering when nothing changed, including when two updates cancel out.
  • Counting a failed render (the error ones) in the render number.

Your exercise

The starter reads the script and queues updates. You write three small functions: use_state, which is the cursor-and-slot logic; render, which runs the body and checks the hook count; and drain, which applies the queues.

Up nextuseEffect HookHooks & State

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…