The clearest mental model for React is to separate two things: *the component a developer write and the engine that runs it. A component is a pure-ish function; the React engine (the runtime and reconciler) is the impure machine wrapped around it. Hooks are the interface between the two.
The pure core
A component is a function of its props and its state, and the value it returns is merely a description of the UI (JSX), not the UI itself:
JSX = render(props, state)
Rendering is just computing that description - the declarative heart of React. The function re-runs from scratch on every render, its local variables reset each time, and given the same inputs it must return the same output.
By itself, this pure function has no memory and no effect on the outside world. It cannot remember a value between renders, talk to the network, or read a value injected from above. Everything stateful or effectful is supplied from elsewhere.
The impure engine wraps it on both sides
That “elsewhere” is the engine, and it surrounds the component rather than sitting downstream of it:
┌──────────────────── React engine (runtime) ──────────────────────┐
│ • calls the component • stores hook state on the fiber │
│ • reconciles (diffs) JSX • commits changes to the DOM │
│ • runs effects • reschedules a render on setState │
└──────────────────────────────────────────────────────────────────┘
│ calls(props) ▲ returns JSX
▼ │
props ─► render(props, state) ─────────────┘
▲
│ hooks = requests to the engine
└── the engine supplies the values and remembers them
- Upstream, the engine calls the component and answers its hook calls. The state does not live in the function - the engine holds it on the component’s fiber, matched by the order the hooks are called (see the array-of-state-pairs mechanism under “Preserving and resetting states” in the main note).
- Downstream, the engine takes the returned JSX, diffs it against the previous tree, commits the differences to the DOM, and runs the effects.
- As a loop, a
setStatecall schedules another invocation of the component. It is a cycle, not a straight line.
So the component genuinely has no state of its own. When it calls useState, it is not creating storage; it is requesting the slot the engine owns and asking to be handed the current value.
Hooks are everything a pure render cannot do itself
Because render is meant to be a pure props → JSX mapping, hooks are precisely the set of escape hatches for what that mapping cannot do alone. A component that is a pure mapping needs zero hooks; you reach for one the moment you need:
- Memory across renders -
useState,useReducer,useRef. - Side effects and their lifecycle (talking to the outside world) -
useEffect,useSyncExternalStore. - Injected services / dependencies from an ancestor -
useContext. This is dependency injection, one of the classic design patterns: pull a dependency from an ambient provider instead of threading it through every prop. - Stable identity and optimisation -
useMemo,useCallback,useId. - Reusable bundles of the above - custom hooks, which package stateful logic (not state itself) into a feature.
props and the hooks are the same kind of thing seen from the component’s side: both are inputs to the pure render. Props are the explicit inputs handed down by the parent; hooks are the implicit inputs and capabilities supplied from the side by the engine.
The deep analogy: algebraic effects
The relationship is React’s userland approximation of algebraic effects. The component performs an effect - “I need some state”, “I need this context” - and the engine is the handler that suspends the call, fulfils it from its own bookkeeping, and resumes the function with the value. This is why hooks feel slightly magical: the function looks pure and synchronous, but a runtime is intercepting those calls and supplying the answers. It also explains the rules of hooks - call order has to be stable because the handler identifies each request by position.
The practical payoff is the functional core, imperative shell pattern. The pure view computation sits in the middle; all the impurity - memory, effects, the outside world - is quarantined at the edges and reached only through hooks. Keeping “the function I write” mentally distinct from “the machine that runs it” is the single most clarifying distinction in React, and hooks are the seam between them.