Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 57

Mastering useState: State Management and Re-renders

Recommended resources

Introduction

Welcome back! In our previous lesson, we established the fundamental "Rules of Hooks" and uncovered the "why" behind them: React relies on a stable call order to associate state with the correct hook, using a linked list of hooks stored on a component's internal fiber node.

Today, we'll zoom in on the most foundational hook: useState. Our goal is to move beyond simply using it and to describe how useState manages component state and triggers re-renders. True to your interest in the underlying mechanics, we'll dissect the entire process—from the initial state creation during a component's "mount" to the update-and-render cycle that happens when you call the setter function.

By the end of this lesson, you'll have a precise mental model of the lifecycle of a piece of state in React, understanding exactly what happens when you call [count, setCount] = useState(0) and later, setCount(1).


The Render and Commit Cycle: A Quick Recap

Before we dive into useState's specifics, let's frame the process. When you update state, React kicks off a re-render. This isn't a single, instantaneous event but a two-phase process.

React Re-render Phases on State Updates
This diagram shows the three high-level phases: a **Trigger** (like a state update), the **Render** phase where React computes changes, and the **Commit** phase where it applies those changes to the DOM.

When setCount is called, it acts as the trigger. This leads to the render phase, where React re-executes your component function to determine what, if anything, has changed. Finally, in the commit phase, React takes the calculated changes and updates the actual DOM.

React Re-rendering Process Flow
This diagram details the render phase. React creates a new "virtual DOM" tree and compares it to the previous one to find the minimal set of changes needed for the commit phase.

Our focus today is on how useState initiates this trigger and how it provides the new state during the subsequent render phase.


Part 1: The Initial Render (Mounting State)

In the last lesson, we learned that hooks are stored as a linked list on a component's fiber node. Let's see how useState creates the first entry in that list.

When your component runs for the first time (the "mount" phase), the useState call executes mountState internally. This function has a clear job:

  1. Create a new hook object.
  2. Store the initial value you provided (e.g., 0) in hook.memoizedState.
  3. Create an update queue within the hook object to hold future updates.
  4. Create and return the dispatch function (your setCount), binding it to the current component's fiber and the newly created queue.
  5. Link this new hook object to the component's fiber node. If other hooks are present, it's added to the end of the linked list.

To see a great narrative of this process, let's turn to an article that connects the concepts of fibers and hooks.

A journey through the implementation of the useState hook

The following article, 'A journey through the implementation of the useState hook', clearly explains how React associates state with the component's fiber node using a linked list. It's a great bridge from our last lesson to the deeper mechanics we're exploring now.

Please read the section titled 'Updating the hook'. While its title is about updates, this section provides the clearest explanation of how React manages hooks for a component. Focus on: The role of the fiber object. How the memoizedState property on the fiber points to a linked list of hook objects. How the next property on each hook object creates this chain. This directly explains why the Rules of Hooks are so critical.

After this initial render, the component's fiber node now holds a hook object in its memoizedState linked list. This object contains both the current value (memoizedState) and an empty queue waiting for updates.


Part 2: The Update Process

Now for the most interesting part: what happens when you call setCount(count + 1)?

It's a common misconception that this function immediately changes the count variable. It does not. Instead, it dispatches an update and schedules a re-render.

The process looks like this:

  1. The setCount (or dispatch) function creates an update object. This object contains the new value or the updater function you provided (e.g., c => c + 1).
  2. This update object is pushed into the pending queue of the corresponding hook object.
  3. React's scheduler is notified that this component's fiber is "dirty" and needs to be re-rendered. This is done by calling scheduleUpdateOnFiber. The re-render doesn't happen synchronously; the scheduler decides when to do it (though for user events, it's typically very fast).
  4. Later, when the scheduler processes the re-render, it calls your component function again.
  5. When the useState line is executed this time, React sees it's an update render. It calls updateState internally, which processes the update objects in the hook's queue, calculates the new state, and returns it.
  6. The new state is then stored in hook.memoizedState for the next render.

This entire flow is complex, and the best way to understand it is to see a guided walkthrough of the actual internal logic.

React Internals Deep Dive 5 - How does useState work internally?

The video 'React Internals Deep Dive 5 - How does useState work internally?' by JSer is an excellent, code-level exploration of this entire process. It's dense, but it will give you the precise conceptual model you're looking for.

This is a detailed video, so focus on these specific segments: Mount Phase (06:34 - 10:20): Briefly watch this part. The key takeaway is seeing the creation of the hook object with its memoizedState and queue. This visualizes the concepts from the article you just read. Update Phase (15:36 - 25:10): This is the most critical part of the lesson. Watch carefully to see how clicking the button triggers dispatchSetState. Observe how an update object is created and how scheduleUpdateOnFiber is called. This is the 'trigger' phase. Re-render Processing (25:10 - 35:30): Continue watching to see the re-render. The key function here is updateState (which calls updateReducer). Notice how it processes the queue to compute the new state, which is then returned from the useState call. Bonus: The Updater Function (51:30 - 52:49): Finally, watch this short segment. It explains exactly why using a function (setCount(c => c + 1)) is safer, connecting it directly to the fact that updates are queued and processed in batches.

For a textual reference that you can review, the companion article to this video provides annotated source code for these exact functions.

How does useState() work internally in React? - JSer.dev

This article, 'How does useState() work internally in React?', is the written version of the video. It's a great resource to solidify your understanding of the code flow.

You don't need to read this in-depth right now, but skim the sections '1. useState() in initial render(mount)', '2. What happens in setState()?', and '3. useState() in re-render'. Notice how the functions mountState, dispatchSetState, and updateReducer map directly to the process we've just discussed. Keep this article as a reference for when you want to review the specific implementation details.


Conclusion

By looking under the hood, we've replaced the "magic" of useState with a concrete, logical process.

Key Takeaways:

  • Mounting: On the initial render, useState creates a hook object with a memoizedState and an update queue, linking it to the component's fiber node.
  • Updating: The setter function (setCount) is a dispatch function. It doesn't change state directly. Instead, it creates an update object, adds it to the hook's queue, and tells React's scheduler to re-render the component.
  • Re-rendering: During the next render, useState processes the updates in the queue to compute the new state, returns it, and updates the memoizedState on the hook object for future renders.
  • Functional Updates: Using an updater function (e.g., setCount(c => c + 1)) is robust because React queues the function itself, not a potentially stale value. This ensures calculations are always based on the most recent state in the queue.

This model of dispatching updates to a queue is a core pattern in React and is also fundamental to how useReducer works, which we will touch on in a later module.


What's Next?

We've now covered how to manage state that is directly tied to a component's render output. But what about actions that aren't part of the main render logic, such as fetching data, setting up subscriptions, or manually manipulating the DOM? These are called "side effects." In our next lesson, we will explore the useEffect hook, which is React's primary tool for managing side effects within the component lifecycle.