We Don’t Know How React State Hooks Work


We use them all the time, and we think we know state hooks work (useState, useReducer). But under the hood, as expected, things are not what you imagine. This talk is about update queues, batching, eager and lazy updates, and some other cool things learned from looking at Hooks source code. It also contains practical takeaways that will help you better understand and debug your code.

28 min
25 Oct, 2021


Sign in or register to post your comment.

AI Generated Video Summary

This talk explores how useState works under the hood and why it's important to understand. It addresses the common confusion around the callback to setState and provides insights gained from exploring React hooks source code. Knowing how useState works is important for learning patterns, debugging, and gaining confidence in our code. React manages the current value of hooks in a linked list and performs updates sequentially. React optimizes rendering by caching computations and performing shallow renders when the state doesn't change.

1. Introduction to useState

Short description:

This talk explores how useState works under the hood and why it's important to understand. It addresses the common confusion around the callback to setState and provides insights gained from exploring React hooks source code.

Hi, this is Adam Klein, and this is We Don't Know How UseState Works. So, this talk was born after I heard many times people asked this question on different forums. What about the callback to setState? So people were migrating from using classes and calling setState, to using hooks and calling useState. And they were missing this feature that you can add a callback after the state is updated. People asked, how do I do that with hooks? Which made me realize that people just don't know how useState works under the hood. I didn't know how useState worked under the hood until I started noticing some strange things when doing console logs. And I decided to open up React hooks source code. And I found out some great things under the hood. Which made me realize if people knew this, they wouldn't ask about the callback of setState.

2. Importance of Understanding useState

Short description:

Knowing how useState works under the hood is important for several reasons: it's fun, it allows us to learn patterns that can be applied to other projects, and it helps with debugging and gaining confidence in our code.

So before we dive into, let's ask ourselves, why do we need to know how it works under the hood? I mean, we're using it, we know how to use it. So there are three reasons, in my opinion. First of all, it's fun. It's fun to know and get extra knowledge about the framework that we use every day. It demystifies the magic. The second thing is that we might see patterns that we can learn from and implement in our own projects, which are unrelated to React, even. And the third, I think, is that when you're debugging code and weird stuff happens, it's very helpful to know how it works under the hood and gain more confidence about the code that you write.

3. Understanding setCounter

Short description:

Let's explore what happens when we call setCounter in a simple component using useState. React manages the current value of hooks in a linked list and triggers a rerender after updating the hook's value. However, this common understanding is incorrect.

So let's take this code, which is a simple component, and it uses a state, which is called the counter, and we have a button with an onClick event handler. And in the onClick event handler, we call setCounter, and we pass in an updater function.

Now, this could also be a value, but as you know, useState also supports updater functions, which receive the previous value and return a new value. So let's dive into what happens when you call setCounter. And this is what I used to think.

Now, I know and you might know that React manages the current value of the hooks for us in some kind of ordered data structure, which is a linked list of hooks, and they are called by the order. So what I thought happens is we call setCounter. React sees that we passed in an updater function, so it takes the current value, which is zero, passes it to the updater function, gets the new value, which in our case is one, updates the current value of the hook to one, and then triggers a rerender of our component, and our component calls usetate again and gets the new value, which React stores, which is one. So it's pretty simple. The only thing is, it's wrong. This is not how it happens.

4. Understanding setCounter in React

Short description:

When calling setCounter in React, the update function is added to a queue and a rerender is scheduled. The component is rerendered after all updates are processed. useState checks for queued updates during the render and performs them sequentially. This explains the order of rendering and updating in React components.

So before I show you how it happens, let's look at some console logs. Here we have a component which does pretty much the same thing, and let's see what happens when we click the plus button. We have an onclick event handler, and we call setcounter. Let's minimize this with the update function. Here we have a console log of update, and here we have a console log of render. And when we click the increment button, let me just move this over here, we render first, and then we call the update function, which is exactly the opposite of what we expect, right?

We expect it to update the value of the hoop and then re-render, but it's the other way around. So let's see how this happens. So when we call set counter, there's actually another linked list attached to each of our hooks, and this is the update queue. So every update that we perform on our state is not performed immediately. It's added to the queue, and it will be processed during render. Let's see how this works.

So we call set counter, we pass in the update function. All React does is put this update function in a queue, and then it schedules a rerender, which means it turns on some kind of flag that says, this component needs to be rerendered after we finish all of the updates. Now we might have more updates. We might update this hook again. We might update other hooks. We might call the parent component callback, which will update some stuff. We might dispatch some Redux action that will update other components. And all of these updates will be queued. And then at some point, and we will discuss when it happens. React decides to rerender the entire component tree, or not the entire component tree, the components that were scheduled for rerender from top to bottom. So according to the hierarchy of the component tree. And as I mentioned, we will discuss when this happens later. And now, and only now, when we actually re-render the component, React looks at what we call useState. When we actually call useState, during the render, React checks to see if there are any updates that are queued, and only then it will perform these updates. So during render, while we call useState of this specific hook, React will look and see, okay, there's an action, let's perform it, this action updates the hook to one, and if there are other actions, it will perform them sequentially until we exhaust the queue. So let's add another console log, minimize this. So now, as you can see, we added another console log after useState. So we console log before useState, after useState, and during the updater function, and let's do some updates, clear the log, and when we click plus, you can see that the first thing that happens is that we render. The second thing is that we call update, so which means that during the call to useState, React runs the updater function, and only then we call after useState, which is here.

5. Updating State and Mimicking setState

Short description:

Every update to a hook is queued and performed lazily during hook invocation. The state is updated only during render. useEffect or useLayoutEffect can be used to perform actions after the state is updated. This is the correct way to mimic the behavior of the updated function of setState.

It's always good to see for yourself what people teach you. So to summarize this part of the talk, every update to a hook is queued. After all updates are done, the components rerender, and the updates are performed lazily during hook invocation. Lazily means that React performs them only at the exact time when they are needed, and not before.

So now we can, first of all, answer this question. How do we call the callback to the setState when we're using useState? So now we can answer, when is the state updated? Only during render. So how can we do something after the state is updated? We call useEffect or useLayoutEffect, which is something to be performed after the render happens. Okay. And obviously we can pass this state as a dependency of the useEffect so we can know when this state was updated exactly, and we can react to it. So this is a standard way to mimic the behavior of the updated function of setState. It's the correct way to do it.

6. Understanding renderBailout

Short description:

React eagerly calculates the new state before triggering a rerender. If the new state is different, it bails out of the render. React optimizes render time by heuristics. The new calculated value is cached in the update queue, avoiding recomputation. The first time a value is updated, React re-renders if a new value is returned. Subsequent updates dismiss this optimization.

What about renderBailout? So here, let's say we have again a counter, but we set it to the exact same value. Now we expect React to know that it's the same value and not trigger a rerender of our component. But we already said that React only runs the updates during the component's rerender, so how can it even bail out or rerender the component if it calculates everything lazily? It doesn't even know that we set it to the same value.

So I cheated. There's actually another step that happens sometimes, which is that React eagerly calculates the new state before triggering a rerender. And if it determines that the new state is different than the old state, it will bail out of the render and it will not trigger a rerender. Obviously, it won't do it if we already scheduled a rerender for this component. There's no need to do this eager calculation, because we need to rerender anyway. And also sometimes React will determine that it's not worth doing the calculation. For example, if we've already called the same callback a few times and every time it returns a new value, React just assumes that it will return a new value. So it's just heuristics that React runs to optimize our render time.

So, obviously, if the new calculated value is different, React will schedule a rerender and will continue the flow, except that because it already computed the new value, it will cache the result inside the update queue. So when we will actually call useState and perform the update queue, React already has this result inside the queue. So it still didn't update the actual value of the hook, but it did store the result of this specific action. So we call the action, we don't have to compute it again because the result is queued. We update the value of the hook and we continue with the render. So now, the reason I say I cheated, is because every time I showed you console logs, I clicked the plus button a few times, to avoid showing you this weird behavior that happens the first time you update the value.

So the first time I clicked, you can see that, the first thing that happens is that React tries to update the state. It calls the update function. So for the first time, React says, let's see what happens. And it does the eager calculation, and it sees that the new value is equal to the previous value and it re-renders the components, and only then it shows after useState. Now, the reason that it re-renders is because we actually returned a new value. So we had zero and the new value is one. So it says, okay, we need to re-render the component. And then when we call useState, one is already cached. So React doesn't call the update function again. And then we get to the after useState. So this is for the first time. The second time, React already dismisses this optimization, because the last time we called the update function, it returned a new value. So it just assumes that the next time it will also return a new value.

7. Optimization and Shallow Rendering in React

Short description:

React optimizes the rendering process by computing eagerly and caching the computation. If the state doesn't change, React performs a shallow render and stops further re-rendering. This behavior is explained in the React documentation.

So I don't know exactly how React optimizes this. I didn't see any other patterns. Obviously, it's something that they can change all the time. So it's not part of the documentation, and you can't really rely on how this optimization will work. And so what if we calculated the state lazily? But still it doesn't change. So React didn't do the eager calculation. It did lazily, but still it didn't change. So does it still re-render everything, even though the only thing that happened was we updated the state to the same value? A long question, short answer, no. It will not continue to re-render. It will just do a shallow render of this component, and if it will find out that nothing actually changed, it will stop there. And you can also check out the React documentation that explains exactly this. So to summarize this part, React sometimes computes eagerly to bail out of render. It caches the eager computation to avoid re-computing. And anyway, if the re-computation is done, if the computation is done lazily and the state didn't update, it will bail out of render and just complete the shallow render of our component.

8. When does React re-render? What about reducers?

Short description:

When does React re-render? React decides to re-render after updating is complete. Event handlers in React run in batches, and every update to a hook is queued. Async functions trigger immediate re-renders. To avoid flickering and optimize renders, use synchronous event handlers or react-dom unstableBatchUpdates. A bug in older React versions occurs when using recycled events inside updated functions. Upgrade to new versions or extract values before using them.

Part three. When does React re-render? So I promised you that I will explain when this happens, so we schedule the re-render, but when does React actually decide, okay, we're done with updating, let's re-render? So again, let's take an example. We have an onclick event handler. And every event handler in React runs inside a batch, which means that when we call this onclick, React runs it inside some kind of batch function internally. And every update to a hook will be queued. So the first time we call setCounter, it will add an action to the queue. The second time we call setCounter, it will add an action to the queue. And after the function finishes, it will trigger a re-render. So you probably ask yourself, OK, but what about async functions? If we call something async, then the actual function is complete before we get to the asynchronous code. So React can't really know that you are trying to run updates to the state. It doesn't know when this async function will complete. So the batch is done immediately, and then every call to setCounter will trigger a rerender to the component. So in this case, with an async function, we will actually rerender the component twice. So this is important to know in case you want to avoid some kind of flickering of the UI, or you want to micro-optimize your renders. And then you should know that this will trigger a render twice. It's usually not a problem, but it helps to be aware of the difference between synchronous functions and asynchronous functions. And if you really want to do this inside a batch, you can call react-dom unstableBatchUpdates. And this will do the same behavior as React does when you actually call a synchronous event handler. And so when you call unstableBatchUpdates, it will only render once, even though you updated the counter twice. The only thing is that this is a function called unstable underscore something, it obviously deters us from using it. But it's pretty safe to use it if you really need to, because React, the team realizes that a lot of people use it and some libraries and frameworks use it. So they actually declared it not as part of the public API, but they don't make breaking changes to this API because they realize a lot of people use it. And also they are going to support in the new versions of React, batching updates in a more stable way. So bonus step is, where is the bug? This is more relevant to previous versions of React, but a lot of you are probably still using these versions. So what is the bug with this code? We have an event handler and we call setstep, passing an updated function and we use e.target.value. The problem with this code, again, in previous React versions, is that we are using E, which is an event, and React recycles the events, which means that, and now we know that this updated function will only run during the next re-render, where React already could have reused the synthetic event that is using inside the event handler. So the way to solve this is, first of all, upgrade to new React versions, when you don't have this problem. And the second result, if you can't migrate, is just to extract the value before and then use it inside the updated function, or call persistEvent, which persists the event and prevents it from being re-rendered. So to summarize this part, React performs event handlers in a batch, async code won't run in a batch unless we use unstable batched updates or the new APIs from React, and events are recycled, don't use them inside the updated functions in older versions of React.

Part 4, what about reducers? So the answer is pretty simple, reducer is actually, state, is actually a reducer under the hood.

9. Understanding setState and Reducer

Short description:

The setState function is a dispatch function that takes the value or updated function as an argument. The reducer in setState, called basicStateReducer, uses the previous state and the action to determine whether the action is a function or a new value. If it's a function, it calls the function with the previous state and returns the result. Otherwise, it returns the new value. This behavior applies to both useState and useReducer.

And the setState function is actually a dispatch function, we're dispatching the value or the updated function, and the reducer of setState just takes the value and uses it on the previous value. So this is actual code from React, this is the reducer that useState uses, it's called basicStateReducer, it takes the previous state and the action, and then it checks if the action is a function which means we passed in an updated function, it just calls this action with the previous state and returns the result, otherwise we just passed in the new value so it just returns the new value. So as you can see, we check if it's a function, if it's a function we call it, otherwise we just return the value. Which means that everything that we said about how useState works, it holds exactly the same for useReducer.

10. Reducer Execution and Render Cycle

Short description:

If we dispatch an action, the reducer may not always run. There are scenarios where the render cycle may be interrupted, or the component may be unmounted before the render cycle. In such cases, the queued updates are dismissed, and the reducer doesn't run. So if you're not seeing the console log inside the reducer after dispatching an action, this could be the reason.

And now another question, if we dispatch an action, will the reducer always run? Can we guarantee that the reducer will run if we dispatch an action? So the answer is no, because we only call the reducer during the render cycle. And we might not even get to the render cycle. First of all, there might be some exception that will stop the render in the middle. And second, our elements might be even unmounted in the next render cycle, because remember, we call setState, but we can also call some kind of callback in our parent component. And maybe this callback will change some kind of flag that will hide our component in the next render cycle. So we might not even call the render function of this component again, which means that we just dismiss the queue of the updates, and the reducer won't even run. So again, if you dispatch an action and your console log inside the reducer, and you don't see the console log, now you know why.

11. Asynchronous State Updates in React

Short description:

State updates in React can be asynchronous or deferred depending on the context. When calling set counter, the update may happen synchronously or asynchronously, depending on concurrent mode and batching. The rendering process in React can be delayed or deferred to prioritize other tasks. This complexity can lead to confusion about the synchronous or asynchronous nature of state updates.

So to summarize, state is reducer, everything we said about state calls for reducers. And the next part is a bit philosophical. People keep saying that state updates are asynchronous in React. And if you search, google it, you get a lot of different answers, which determine that setState is asynchronous, which is pretty true, but there are a few nuances to know.

So like everything in Facebook, it's complicated. So state updates may be asynchronous. Let's just look at some console logs and describe it the best way we can. So now what I want to show you is an on-click event handler. I call promise resolve and console.log. So this will be logged immediately after the current event loop, which means after all of the synchronous tasks. Then we call set counter and we call after updates. Let's clear the log and see what happens when we call the plus. So as you can see, we called set counter twice. We console logged it twice and these are the first things that we log inside our console. Which makes sense because they happened synchronously.

You can also see that the render happened here and it happened before the async code, which was here. Which means that during the same event loop, synchronously React called our event handler, triggered the re-render, and called our render function of the component. Everything was performed synchronously inside the same event loop. And only after the render was completed, then we saw this async log of promise. Which confuses us a bit, because we just said that state updates are asynchronous, but we just saw that it happens synchronously.

So there are a few answers to this question. First of all, in concurrent mode, it could be asynchronous in a different event loop. Because React may delay the rendering of our components in favor of more prioritized tasks. So it could actually be performed truly asynchronously. And when we do batching, you can look at it as deferred. It's not truly asynchronous in terms of the event loop, but it's deferred. We call the set counter, and then we move on to our next statement. And the renders still didn't happen. So it feels asynchronous. So again, this is a very complicated explanation.

12. Understanding State Updates

Short description:

State updates in React can be considered asynchronous. They are queued and computed lazily during render. React bails out of render if the state doesn't change. Updates are batched inside Event Handlers, and state is actually a reducer.

It's much easier to just think of it as asynchronous and this is what basically React recommends us. First of all, because of concurrent mode where it can truly be asynchronous. And second of all, because it's implementation details. It doesn't want us to rely on the fact that it happens during the same event loop. So the best way to simplify it is to think of state updates as asynchronous.

To summarize this part, state updates are queued and computed lazily during render. React bails out of render if the state doesn't change. Updates are batched inside Event Handlers, and state is actually a reducer. And lastly, state updates should be considered asynchronous.

Thank you. I hope you enjoyed the talk. I surely did, and I hope you learned some cool stuff. And if there are any questions, let me know. And see you all next time.

Check out more articles and videos

We constantly think of articles and videos that might spark Git people interest / skill us up or help building a stellar career

React Summit Remote Edition 2021React Summit Remote Edition 2021
33 min
Building Better Websites with Remix
Remix is a new web framework from the creators of React Router that helps you build better, faster websites through a solid understanding of web fundamentals. Remix takes care of the heavy lifting like server rendering, code splitting, prefetching, and navigation and leaves you with the fun part: building something awesome!
React Advanced Conference 2022React Advanced Conference 2022
25 min
A Guide to React Rendering Behavior
React is a library for "rendering" UI from components, but many users find themselves confused about how React rendering actually works. What do terms like "rendering", "reconciliation", "Fibers", and "committing" actually mean? When do renders happen? How does Context affect rendering, and how do libraries like Redux cause updates? In this talk, we'll clear up the confusion and provide a solid foundation for understanding when, why, and how React renders. We'll look at: - What "rendering" actually is - How React queues renders and the standard rendering behavior - How keys and component types are used in rendering - Techniques for optimizing render performance - How context usage affects rendering behavior| - How external libraries tie into React rendering
React Advanced Conference 2022React Advanced Conference 2022
30 min
Using useEffect Effectively
Can useEffect affect your codebase negatively? From fetching data to fighting with imperative APIs, side effects are one of the biggest sources of frustration in web app development. And let’s be honest, putting everything in useEffect hooks doesn’t help much. In this talk, we'll demystify the useEffect hook and get a better understanding of when (and when not) to use it, as well as discover how declarative effects can make effect management more maintainable in even the most complex React apps.
React Summit 2022React Summit 2022
20 min
Routing in React 18 and Beyond
Concurrent React and Server Components are changing the way we think about routing, rendering, and fetching in web applications. Next.js recently shared part of its vision to help developers adopt these new React features and take advantage of the benefits they unlock.
In this talk, we’ll explore the past, present and future of routing in front-end applications and discuss how new features in React and Next.js can help us architect more performant and feature-rich applications.
React Advanced Conference 2021React Advanced Conference 2021
27 min
(Easier) Interactive Data Visualization in React
If you’re building a dashboard, analytics platform, or any web app where you need to give your users insight into their data, you need beautiful, custom, interactive data visualizations in your React app. But building visualizations hand with a low-level library like D3 can be a huge headache, involving lots of wheel-reinventing. In this talk, we’ll see how data viz development can get so much easier thanks to tools like Plot, a high-level dataviz library for quick
easy charting, and Observable, a reactive dataviz prototyping environment, both from the creator of D3. Through live coding examples we’ll explore how React refs let us delegate DOM manipulation for our data visualizations, and how Observable’s embedding functionality lets us easily repurpose community-built visualizations for our own data
use cases. By the end of this talk we’ll know how to get a beautiful, customized, interactive data visualization into our apps with a fraction of the time

React Summit 2023React Summit 2023
24 min
React Concurrency, Explained
React 18! Concurrent features! You might’ve already tried the new APIs like useTransition, or you might’ve just heard of them. But do you know how React 18 achieves the performance wins it brings with itself? In this talk, let’s peek under the hood of React 18’s performance features: - How React 18 lowers the time your page stays frozen (aka TBT) - What exactly happens in the main thread when you run useTransition() - What’s the catch with the improvements (there’s no free cake!), and why Vue.js and Preact straight refused to ship anything similar

Workshops on related topic

React Advanced Conference 2021React Advanced Conference 2021
132 min
Concurrent Rendering Adventures in React 18
Workshop Free
With the release of React 18 we finally get the long awaited concurrent rendering. But how is that going to affect your application? What are the benefits of concurrent rendering in React? What do you need to do to switch to concurrent rendering when you upgrade to React 18? And what if you don’t want or can’t use concurrent rendering yet?
There are some behavior changes you need to be aware of! In this workshop we will cover all of those subjects and more.
Join me with your laptop in this interactive workshop. You will see how easy it is to switch to concurrent rendering in your React application. You will learn all about concurrent rendering, SuspenseList, the startTransition API and more.
React Summit Remote Edition 2021React Summit Remote Edition 2021
177 min
React Hooks Tips Only the Pros Know
The addition of the hooks API to React was quite a major change. Before hooks most components had to be class based. Now, with hooks, these are often much simpler functional components. Hooks can be really simple to use. Almost deceptively simple. Because there are still plenty of ways you can mess up with hooks. And it often turns out there are many ways where you can improve your components a better understanding of how each React hook can be used.
You will learn all about the pros and cons of the various hooks. You will learn when to use useState() versus useReducer(). We will look at using useContext() efficiently. You will see when to use useLayoutEffect() and when useEffect() is better.

React Advanced Conference 2021React Advanced Conference 2021
174 min
React, TypeScript, and TDD
Workshop Free
ReactJS is wildly popular and thus wildly supported. TypeScript is increasingly popular, and thus increasingly supported.
The two together? Not as much. Given that they both change quickly, it's hard to find accurate learning materials.
React+TypeScript, with JetBrains IDEs? That three-part combination is the topic of this series. We'll show a little about a lot. Meaning, the key steps to getting productive, in the IDE, for React projects using TypeScript. Along the way we'll show test-driven development and emphasize tips-and-tricks in the IDE.

React Summit 2023React Summit 2023
171 min
React Performance Debugging Masterclass
Workshop Free
Ivan’s first attempts at performance debugging were chaotic. He would see a slow interaction, try a random optimization, see that it didn't help, and keep trying other optimizations until he found the right one (or gave up).
Back then, Ivan didn’t know how to use performance devtools well. He would do a recording in Chrome DevTools or React Profiler, poke around it, try clicking random things, and then close it in frustration a few minutes later. Now, Ivan knows exactly where and what to look for. And in this workshop, Ivan will teach you that too.
Here’s how this is going to work. We’ll take a slow app → debug it (using tools like Chrome DevTools, React Profiler, and why-did-you-render) → pinpoint the bottleneck → and then repeat, several times more. We won’t talk about the solutions (in 90% of the cases, it’s just the ol’ regular useMemo() or memo()). But we’ll talk about everything that comes before – and learn how to analyze any React performance problem, step by step.
(Note: This workshop is best suited for engineers who are already familiar with how useMemo() and memo() work – but want to get better at using the performance tools around React. Also, we’ll be covering interaction performance, not load speed, so you won’t hear a word about Lighthouse 🤐)
React Advanced Conference 2021React Advanced Conference 2021
145 min
Web3 Workshop - Building Your First Dapp
Workshop Free
In this workshop, you'll learn how to build your first full stack dapp on the Ethereum blockchain, reading and writing data to the network, and connecting a front end application to the contract you've deployed. By the end of the workshop, you'll understand how to set up a full stack development environment, run a local node, and interact with any smart contract using React, HardHat, and Ethers.js.

React Summit 2023React Summit 2023
152 min
Designing Effective Tests With React Testing Library
React Testing Library is a great framework for React component tests because there are a lot of questions it answers for you, so you don’t need to worry about those questions. But that doesn’t mean testing is easy. There are still a lot of questions you have to figure out for yourself: How many component tests should you write vs end-to-end tests or lower-level unit tests? How can you test a certain line of code that is tricky to test? And what in the world are you supposed to do about that persistent act() warning?
In this three-hour workshop we’ll introduce React Testing Library along with a mental model for how to think about designing your component tests. This mental model will help you see how to test each bit of logic, whether or not to mock dependencies, and will help improve the design of your components. You’ll walk away with the tools, techniques, and principles you need to implement low-cost, high-value component tests.
Table of contents
- The different kinds of React application tests, and where component tests fit in
- A mental model for thinking about the inputs and outputs of the components you test
- Options for selecting DOM elements to verify and interact with them
- The value of mocks and why they shouldn’t be avoided
- The challenges with asynchrony in RTL tests and how to handle them
- Familiarity with building applications with React
- Basic experience writing automated tests with Jest or another unit testing framework
- You do not need any experience with React Testing Library
- Machine setup: Node LTS, Yarn