DEV Community

Chandrashekhar Kachawa
Chandrashekhar Kachawa

Posted on • Originally published at ctrix.pro

How Does React's useState Really Work?

The useState hook is often the first thing a developer learns in modern React. It seems like magic: a simple function call that gives a normally stateless function a persistent memory. But how does it really work? How can a value persist between renders when the entire component function is being re-run?

The answer is that React stores your component's state outside of the component itself. Let's pull back the curtain on this mechanism.

The Core Problem: Functions are Stateless

A React functional component is, at its core, just a JavaScript function. In normal JavaScript, when a function finishes executing, its local variables are discarded. If React components followed this rule, they could never hold onto state.

So, when you write const [count, setCount] = useState(0);, the count variable isn't a normal local variable. It's a value that React is managing on your component's behalf.

The "Memory Cell" and The Linked List

For every component instance on the screen, React maintains an internal data structure. You can think of this as a private object for your component, holding its "memory cells." A very common and accurate way to describe this is a linked list of hooks.

  • Each time you call a Hook (useState, useEffect, etc.), it corresponds to a node in this list.
  • React relies on the call order of your Hooks being identical on every single render to know which memory cell belongs to which Hook call.

Let's trace the lifecycle.

Phase 1: The Initial Render

When your component runs for the very first time, React builds this list of memory cells.

Consider this component:

function UserProfile() {
  const [name, setName] = useState('Alex');   // Hook call - Index 0
  const [age, setAge] = useState(30);       // Hook call - Index 1
}
Enter fullscreen mode Exit fullscreen mode

On the initial render, React's internal state for this component instance conceptually looks like this:

Component's Internal State:
- hooks: [
    { value: 'Alex', dispatch: function_for_name }, // Cell at Index 0
    { value: 30,     dispatch: function_for_age  }  // Cell at Index 1
  ]
- hookIndex: 0 // A pointer to track the current hook
Enter fullscreen mode Exit fullscreen mode

When useState('Alex') is called, React sees it's the first hook (hookIndex is 0), creates a cell, stores 'Alex' in it, and returns the value and its associated dispatch function. It then increments the hookIndex to 1. The same happens for the age state.

Phase 2: The Update and Re-render

When you later call setAge(31), you are invoking the dispatch function associated with the second memory cell. This function does two things:

  1. It finds the correct cell (the one at index 1) and updates its internal value to 31.
  2. It schedules a re-render of your UserProfile component.

Now, the component function runs again. This is where the magic happens:

  1. React resets the hookIndex for this component back to 0.
  2. The first line, useState('Alex'), is executed. React checks the cell at index 0, finds the value 'Alex', and returns it. It then increments the hookIndex to 1.
  3. The second line, useState(30), is executed. React checks the cell at index 1. It ignores the initial state argument (30) because the cell already exists. It finds the current value (31) and returns that instead.

This is how your component receives the updated state on a re-render.

This Explains the Rules of Hooks

This internal mechanism is precisely why the Rules of Hooks are not just suggestions—they are technical requirements.

Rule: Only call Hooks at the top level.

You cannot call Hooks inside conditions, loops, or nested functions because it would disrupt the call order.

Consider this broken example:

// DON'T DO THIS
if (userIsLoggedIn) {
  const [name, setName] = useState('Alex'); // Might not run on every render
}
const [timestamp, setTimestamp] = useState(Date.now());
Enter fullscreen mode Exit fullscreen mode
  • Render 1 (user is logged in):

    1. useState('Alex') is called. It's at index 0.
    2. useState(Date.now()) is called. It's at index 1.
  • Render 2 (user is logged out):

    1. The if block is skipped.
    2. useState(Date.now()) is called. It's now at index 0.

React now has a mismatch. It tries to read the state for timestamp from the memory cell at index 0, but that cell actually contains the name state ('Alex'). Your component is now broken in a very confusing way.

By always calling Hooks in the same, unconditional order, you guarantee that React can correctly associate each useState call with its private memory cell on every single render. The "magic" is really just a simple, reliable array or linked list lookup.

Conclusion

In conclusion, the useState hook is more than just a function; it's a window into React's internal state management. By maintaining a consistent, ordered list of 'memory cells' for each component, React ensures that state persists reliably across renders. This design is what makes the Rules of Hooks, particularly the requirement to call hooks at the top level, a technical necessity rather than a mere convention. Understanding this underlying mechanism demystifies the magic and empowers you to write more predictable and robust React components, turning abstract rules into concrete knowledge.

Top comments (0)