React hooks are implemented as a linked list of hook nodes stored in the fiber's memoizedState field, where each hook node contains the hook's value, pending updates, and a pointer to the next node. React matches hooks purely by their position in this list during each render, which is why hooks must be called in the same order every time. The hook implementation comes from a dispatcher object that React injects during render, which only holds a real dispatcher while React is actively rendering the component's hooks; outside this render window, hooks throw errors. This explains why hooks can only be called inside function components and why the order rule exists.
Deep Dive
Prerequisite Knowledge
- No data available.
Where to go next
- No data available.
Deep Dive
React Hooks Are Just a Linked List
Added:Everyone hears the rules of hooks, but the rules sound arbitrary until you see what React actually stores. So, in this video, we're going to open React source and follow one use state call through the fiber, the hook list, and the dispatcher that makes the call work. So, let's start with a component. Here, we have a profile form component, and as we know, React calls this function again on every render. And within that function, we call use state, and React hands back name and set name. But, name is only a local variable, so it only exists while this one call of profile form is running. Then, the input reads that local value. So, if the component runs again and creates a fresh local variable, where does React keep the state between those calls? React keeps that data on a fiber, the object it builds for each component it renders.
So, here's the constructor, fiber node.
It takes in tag, pending props, key, and mode. I trimmed it down just to the fields that matter for this walk-through. This.tag records what kind of fiber this is, and this.type starts as null, then becomes the component function, like profile form, once React creates the fiber from its element.
Then, this.memoizedState, which for a function component is the field that points at the first hook node. It is still null here because before the first render, there's no hook list yet. And this.alternate, which points to the second copy of the fiber that React keeps around for updates. So, for hooks on a function component, these are the two fiber fields that matter. We have memoizedState, which is where React starts when it needs saved hook data.
Once hooks have rendered, this field points at the first hook entry for the component. And then we have update queue, which is separate, and React uses it for effect data and a bit of other hook data. So, not every hook detail lives on memoized state, but the hook list itself starts there. Now, let's look at alternate. On a re-render, React still needs the hook values from the previous render. The one it already finished and put on screen while it builds the new one. So, current is that finished fiber from last time, and work in progress is the new one React is building during this render. Alternate is the link between those two objects.
So, during render, React can read the old hook list from current and write the new hook list onto work in progress.
Then, when this render finishes and goes on screen, the work in progress fiber becomes the new current. So, those freshly written values are what the next render reads. So, memoized state on the fiber points to a list. Each entry in that list is one of these hook node objects. The node's own memoized state is the hook's value. For use state, this is where the current string value is stored. The base state and base queue fields are extra bookkeeping. For updates, React has to skip and replay on a later render. We do not need that full path here, but they are why the node holds more than just the current value.
Then we have queue, which is a per hook storage slot. Use state and use reducer keep their pending updates there. While hooks that do not need it leave it as null. And next points to the following hook node, which links all the hook nodes together. So, the node has a value, some storage, and a pointer to the next node, but no name and no key, meaning there's no internal ID of any kind that ties it to your name variable.
So, let's take a look at how the list is built. On the first render, the hook list is empty. So, React builds it one call at a time. Every hook that lives in this list, like use state, goes through mount working progress hook, and this function returns the node for that call.
First, it builds a fresh node, so const hook, with all its fields starting as null. Then, it checks working progress is equal to null to see whether this is the first hook called this render. If so, then fiber.that.memoizedState is equal to hook makes this node the first one in the list. Otherwise, working progress hook.that.next is equal to hook, so the previous node points at this new node. Then, working progress hook is equal to hook, which moves the cursor, React's running position in the list forward, so the next hook call appends right after this one, and return hook, and start node back to whichever hook called it, like use state. So, let's run a component with three hooks and watch the list build. The first use state for name finds an empty list, so its node becomes hook zero, and the fiber's memoized state points to it.
Then, the second use state for is open appends after it as hook one. And finally, use effect sync title appends as hook two. So, the order of the calls is exactly the order of the nodes. Now, on a normal update, the old list already exists, so React walks that old list while it builds a new one. So, the first hook call starts the old list walk. So, current hook is React's position in the previous list. And on the first call, it is still null. So, React starts it off from fiber.alternate.memoizedState, the head of the old list. Then, every later hook call advances from the current node. So, on those later calls, next current hook is equal to currentHook.next. So, each hook call steps one node forward in the old list.
Now, React needs a node for this render.
So, it clones the old node into new hook, copying memoized state and the other saved fields across. And next starts as null because React is building a fresh list. And then, the clone joins this render's list. So, React adds the clone to the new list the same way as on mount. The first hook becomes the first node, and later ones append after the previous new node. So, then the new list cursor moves. So, working progress hook is equal to new hook makes this clone the last node React has built so far.
And that leaves the return, return new hook, which gives the node back to the hook that called it. So, on every re-render, React matches hooks by just counting through the old list and the new calls in the same order. But, notice one thing. No name is ever passed.
Compare it to helper signatures, mount working progress hook and update working progress hook. None of these take arguments. So, useState, useRef, and useReducer all get their node by calling one of these two helpers. But, the helper isn't handed name, is so pen or whichever label you choose. So, with a hook node stores no variable name and no ID of any kind. What pins a hook to its node is just its position in the list, not whatever you happen to name the result. Now, there is one caveat to all of this. Not every public hook actually appends a node to memoize state. For example, use context with our theme context reads context a different way.
So, it doesn't append on one of these hook nodes. And React 19's use resource is also handled specially. So, for the rest of this walk-through, we're talking about the hooks that do live in the list like use state where matching by position is what ultimately matters. So, so far we have been inside the reconciler, React's core engine where the hooks are actually implemented. But, the use state you import comes from a separate package just called React. So, let's look at that wrapper. Use state, as we can see, takes in the initial state, but it doesn't implement state itself. It does const dispatcher is equal to resolve dispatcher and then return the dispatcher. dot use state and we pass in the initial state as is. And resolve dispatcher is just return React shared internals. h. So, during render, h holds the dispatcher object that has the real hook implementation. So, the public call reads h, then hands off to that dispatcher's own use state. And whatever object is sitting in h decides which implementation actually runs. So, what does h hold? Well, h is a field on React shared internals, one shared object that both the React package and the renderer, like React DOM or React Native, use. H null is the slot for the hooks dispatcher, and it starts as null because the React package only ships the thin wrappers, while the reconciler, which again would be React DOM or React Native, fills in the real implementations during a render. So, dependency injection, if you will. Now, a dispatcher is just a map from public hook names to the functions React should run for this render. When H is hooks dispatcher on mount, the dispatcher React uses on a component's first render, the use state wrapper reaches mount state. And when H is hooks dispatcher on update, the one for every render after that, the same public use state call reaches update state instead.
Now, there's one shared object, but there are two sides. So, the public wrapper reads from H, and the reconciler writes to H. The reason that works is that both sides import the same internal object. The React package creates React shared internals and exposes that internal object to the reconciler, and then the reconciler imports that very same object. So, nothing's copied between them. So, when the reconciler writes H, the use state wrapper in React reads that same field immediately. So, let's take a look at how the render sets H. Every function component render goes through render with hooks, which wraps the call to your component. And the signature is render with hooks. We take in current working progress, then the component, and then the props. So, current is the finished fiber from last time. Working progress is the one for this render. Component is your function, and props are the values React passes in. So, the first thing we do is currently rendering fiber is equal to working progress, which records which fiber is rendering. So, hook functions know which fiber gets the hook list.
Then we set working progress.memoizedState to null, which clears the new list before React rebuilds it for this render. And then we write to H. We write the active dispatcher into this shared slot. And the condition is current is equal to null or current.memoizedState is equal to null. So, if there's no previous fiber or a previous one that never run a stateful hook, React uses the mount dispatcher. Otherwise, it uses the update dispatcher. Then we have const children is equal to components.
We pass in the props as is, which is where your component actually runs and where its hook calls happen. And after that, it calls finish rendering hooks, which cleans up after the render. And then we do our return children, so hand the component output back to the reconciler. So, during the renderers' normal work outside your component's hook calls, H can be context-only dispatcher. Then render with hooks writes the mount or update dispatcher into H. While the component body runs, you state results through that dispatcher. And once React is done rendering the component, finish rendering hooks points H back to context-only dispatcher. And before our renderer, like React DOM or React Native, has said React app at all H is simply null. Now, let's open context only dispatcher. The dispatcher React puts back whenever a component isn't actively rendering its hooks. On this object, ordinary hooks like use state, use effect, and use memo point at throw invalid hook error. The visible exceptions are read context and React 19's use, which as I have already stated have their own paths. So, let's take a look at throw invalid hook error. As we can see, it's just a wrapper around a throw new error. And as we can see, the first sentence is the user-facing rule.
Invalid hook call. Hooks can only be called inside of the body of a function component. And then it lists three possible reasons. Mismatched the versions of React and the renderer, breaking the rules of hooks, or more than one copy of React in the same application. And the last line points you to React's debugging page. For our case, the one that applies is the dispatcher reason. The hook run while H wasn't a real dispatcher. Now, here's that dispatcher rule in ordinary code. A hook call inside an event handler. Let's follow it across the whole life cycle and see exactly when it throws. So, first search box renders. Render with hooks sets H to a real dispatcher. And handle click is just defined here. It doesn't run yet. And the use state is inside handle click, so it only runs when the click happens, not during this render. Then the render finishes. So, finish rendering hooks flips H to context only dispatcher. And the button is committed to the screen. Now, time passes. Nothing is rendering. So, H just stays the context only dispatcher and then the user clicks and handle click finally runs. And inside it, use state reads age, but age is the context only dispatcher now, not a real dispatcher.
So, it finds throw invalid hook error there and throws the invalid hook call right here, long after the render is over. So, the order rule. Now, the order rule follows from the position list.
React matches hooks by counting position by position. So, let's see what an early return does to that. So, last render with show details true, profile form run all three hooks, position zero, one, and two. This render starts the same way.
The first use state always runs, so position zero lines up, but show details can be false on the next render. And when it is, React hits if not show details, then return null and stops right there. So, use state false and use effect sync title never run. So, the last render had three hooks at these positions. This render has one and the counts no longer lines up. So, what happens here is that after the component returns, finish rendering hooks checks whether the render stopped before it got through the whole old list. So, we know last render built three nodes, three hook calls, but this latest render only reached hook zero before the early return. So, current hook is hook zero, but current hook.next, which is hook one, is still there. And that leftover node is exactly what this check catches.
So, if current hook is different from null and current hook.next is different from null, then we throw an error rendered fewer hooks than expected. This may be caused by an accidental early return statement. Now, flip that around.
Say last render took the early return and ran just one hook and the next render runs all three. Well, as each hook runs, update working progress hook walks the old list for the next node to clone. Last render only built hook zero.
So, after it, next current hook is null.
But, this render's second hook still asks for that next node and if next current hook is equal to null is true, there is no node left to clone. So, React throws rendered more hooks than during the previous render. So, the two rules come from two different mechanisms in the source. The first one is that hooks that live in the list are matched purely by their position with no stored name or ID. So, they have to run in the same order every render. And second, every stateful hook finds its implementation through age, which only holds a real dispatcher while React is rendering the component. Any other time, those hooks hit the dispatcher that it throws instead. Now, there's a development only order warning. In development, React remembers the order of the built-in hooks you called on the first render, what it source calls the primitive hooks, then checks the next render's hooks against that list position by position. And when they do not line up, it prints a table just like this one. And those hook names are only tracked in development. In production, React still matches by position. So, the warning points you at the mismatch, but the real mechanism underneath is the same positional list. Now, let's take a look at why a custom hook is fine. So, we have a profile form, and then we do a const width is equal to use window width. And this calls our custom hook down below, and then we return a span assuming the value the custom hook returns. And as for use window width, it's just an ordinary function, of course, and we do a use state call. So, this appends hook zero to profile form's fiber list. And then we have the use effect, which appends hook one. And then we return the width and hand it back to the component. So, the custom hook gets no list of its own. It works because its inner hook calls run during the component's render, and they just take the next positions in that one shared list on the component's fiber. So, that's the model traced through the source. A hook that lives in the list is just a node on the component's fiber, and React matches those nodes by position. And the hook implementation comes from age, which React writes during render and restores afterward.
And a custom hook works because it's just an ordinary function called during the render window. So, the two rules of hooks are about two things: keeping the list position stable and only calling hooks while React is rendering. The one window where age is a real dispatcher.
Anyway, this wraps up the video. If you want to see more deep dives on the internals of React or any other library or framework, do let me know in the comments. I'll see you in the next one.
See you.
Related Videos
Custom Shader FX Nodes - No Boilerplate
git-amend
281 views•2026-06-21
Harsh Reality of GameDev YouTuber
TsodingDaily
629 views•2026-06-19
Course : UVM in Systemverilog 1: L4.1 : Generic UVM Testbench Structure
SystemverilogAcademy
365 views•2026-06-20
microJAM: MAKING A MICRO GAME FOR A GAME JAM IN CLOJURESCRIPT AND TOTALLY NOT C
janetacarr
156 views•2026-06-18
Design Claude Code Like a Senior Engineer
hayk.simonyan
344 views•2026-06-19
Specialist Tries 2600 Rated problems
TejaDronadula
1K views•2026-06-24
Linus Torvalds: AI Won’t Replace Understanding Code
SavvyNik
140 views•2026-06-19
Course : Systemverilog Verification 5 : L9.4 : Count only cross coverage
SystemverilogAcademy
181 views•2026-06-20











