Editor’s note: This article was last by Chimezie Innocent on 12 November 2024 to update code blocks, and explore how to deal with state resets in child components by using key
props and rendering child components in different positions.
When developing React applications, you may have noticed that state updates don’t immediately reflect new values after being changed. This is because state updates are asynchronous, which means that React waits until all codes in the event handler have run before processing and updating the state.
Look at the code below:
const [count, setCount] = useState(0); useEffect(() => { setCount(1); console.log(count); }, []);
The code above logs a 0
in the console. This is because React goes through the code before updating the state. Hence, count
value is 0
on the first render:
This process, known as batching, is used by React to process multiple state updates without triggering too many re-renders. This allows React to update multiple states together, minimizing unnecessary re-renders and optimizing or improving, overall efficiency and performance.
To fix the state update issue above, add count
as a dependency:
useEffect(() => { setCount(1); console.log(count); }, [count]);
The console will log twice — the first time React goes through the code block, count
value is 0
. However, because count
state changed (after the state was updated), React re-renders the page, and now the count
value becomes 1
:
React’s official documentation uses a great analogy to explain this concept:
A waiter taking an order in the restaurant doesn’t go to bring the first order you make. They wait for you to finish ordering before proceeding to bring the order. In the process, these orders could change and the waiter will be sure to add the change. Similarly, they could also take orders from other tables before proceeding to the kitchen.
In this article, we’ll explore the reasons why React doesn’t update state immediately. We’ll demonstrate an example and clarify what you should do when you need to make changes to the new state in both class and functional components. Finally, we will look at dealing with state resets in child components.
As we saw above, state updates are inherently asynchronous processes. When you call the setState
function, React schedules an update rather than immediately applying the changes. When you invoke setState
, React maintains a queue of pending updates. Then, it batches multiple setState
calls that occur within the same synchronous block of code.
This batching process is crucial for preventing unnecessary re-renders and optimizing performance. It calculates the minimal set of changes needed to update the component’s state and applies them in a single, efficient rendering cycle.
To update the state in React components, we’ll use either the this.setState
function or the updater function returned by the React.useState()
Hook in class and function components, respectively.
State updates in React are asynchronous; when an update is requested, there is no guarantee that the updates will be made immediately. The updater functions queue changes to the component state, but React may delay the changes, updating several components in a single pass.
For example, consider the code below:
const [count, setCount] = useState(0); const handleClick = () => { setCount(1); setCount(1); setCount(1); };
There are three setCount
updater functions inside our handleClick
function. However, when the handleClick
event handler is called, count
state updates to 1
.
To understand this, let’s add a console.log
to log our count
state after each updater function:
const handleClick = () => { setCount(1); console.log("log 1:", count); setCount(1); console.log("log 2:", count); setCount(1); console.log("log 3:", count); };
You’ll notice that our console is still logging the old value of count
(0
). This is because on the first re-render, the count
value is 0
and the state will only be updated after the batch process is complete:
Similarly, consider the code below:
const handleClick = () => { setName("Amaka") setAge(20) setAddress("No 3 Rodeo drive") }
In this snippet, there are three different calls to update and re-render the component. Calling the updater functions one after another and re-rendering both parent and child components after each call would be inefficient in most cases. For this reason, React batches state updates.
No matter how many setState()
calls are in the handleClick
event handler, they will produce only a single re-render at the end of the event, which is crucial for maintaining good performance in large applications. The order of requests for updates is always respected; React will always treat the first update requests first.
We’ve established that delaying the reconciliation of update requests in order to batch them is beneficial. Next, we’ll explore managing situations where you must wait for these updates to complete before utilizing the new values.
setState()
callbackThe second parameter to setState()
is an optional callback function. This argument will be executed once setState()
is completed and the component is re-rendered. The callback function is guaranteed to run after the state update has been applied:
handleSearch = (e) => { this.setState({ searchTerm: e.target.value },() => { // Do an API call with this.state.searchTerm }); }
componentDidUpdate
The componentDidUpdate
function is invoked immediately after a state update occurs. To avoid an infinite loop, you should always use a conditional statement to be sure that the previous state and the current state are not the same:
componentDidUpdate(prevProps, prevState) { if (prevState.count !== this.state.count) { // Do something here } }
useEffect()
HookAs we have seen in the introduction section, you can perform side effects in the useEffect
Hook when the state is updated. The state variable could be added as a dependency in this Hook, making it run when the state value changes. You can make the useEffect
Hook listen to the state changes:
import React,{useState, useEffect} from 'react'; const App = () => { const [count, setCount] = useState(1); useEffect(() => { if (count > 5) { console.log('Count is more that 5'); } else { console.log('Count is less that 5'); } }, [count]); const handleClick = () => { setCount(count + 1); }; return ( <div> <p>{count}</p> <button onClick={handleClick}> add </button> </div> ); }; export default App;
The callback function in the useEffect
Hook runs only when there is a change in any of the state variables listed in its dependency array:
Do not use the
useEffect
Hook unnecessarily, especially in this situation.useEffect
is meant for synchronizing your components with external systems, like when you make network and API calls.You can read up more on this on the official docs.
useState()
callbackThe useState
Hook in React doesn’t have a built-in callback like setState
does. However, you can achieve similar functionality using the useEffect
Hook, which allows you to perform side effects after the component has been rendered.
Let’s look at our previous code snippet:
const [count, setCount] = useState(0); const handleClick = () => { setCount(1); setCount(1); setCount(1); };
To update the state, we use an updater function, which is something like this, n => n + 1
, where n
is the value of the old or previous state. See the code below:
const handleClick = () => { setCount((n) => n + 1); setCount((n) => n + 1); setCount((n) => n + 1); };
When the handleClick
function is called, the count
value becomes 3
. This is because when React goes through the code blocks, the count
state changes.
Let’s break this down. On the first setCount
updater function:
n
value is 0 and so, 0 + 1 = 1 (React batches this and holds the count value to be 1)n
value is now 1 and so, 1 + 1 = 2 (React sees that the count value has changed, adds the new count to the previous count, and batches it)n
value is now 2 and so, 2 + 1 = 3 (React again sees that the count value has changed, adds the new count to the previous count, and batches it)Finally, the batch process is completed and React sets the new count
value to 3
.
Additionally, you can name the updater function whatever you like. I prefer to name mine prevState
for better denotation, so it becomes setCount((prevState) => prevState + 1)
.
By default, state is always preserved as long as the component is being rendered or stays in the same position.
Let’s look at the code below:
import { useState } from "react"; import "./styles.css"; export default function App() { return ( <div className="App"> <h1>Hello CodeSandbox</h1> <User /> <User /> </div> ); } export function User() { const [age, setAge] = useState(0); const handleAddUser = () => { setAge((prevState) => prevState + 1); }; return ( <div> <h3>Age: {age}</h3> <button type="button" onClick={handleAddUser}> Add user </button> </div> ); }
We have two child components in our App
. Try clicking on the different buttons and you will see that both children are not updating simultaneously. This is because they are being treated as two different components with different states.
Remember, as long as the components are being rendered or stay in the same position, their state will always be preserved. Let’s look at the code below:
import { useState } from "react"; import "./styles.css"; export default function App() { const [show, setShow] = useState(true); const handleAddUser = () => { setShow(!show); }; return ( <div className="App"> <h1>Hello CodeSandbox</h1> <button type="button" onClick={handleAddUser}> Toggle </button> {show ? <User name="Chimezie" /> : <User name="Innocent" />} </div> ); } export function User({ name }) { const [age, setAge] = useState(0); const handleAddUser = () => { setAge((prevState) => prevState + 1); }; return ( <div> <h3> Name: {name}, Age: {age} </h3> <button type="button" onClick={handleAddUser}> Add user </button> </div> ); }
We are now using a conditional to render both child components. However, we now have names to differentiate when either component is being shown or rendered. Click and toggle between both components, and you will observe that the state is being preserved as long as the component is rendered and stays in the same position.
To reset state in child components, there are several ways we can go about it.
In the conditional example above, we are rendering both components in the same position and that’s why they share the same state and it’s being preserved despite the toggle.
However, if we render them separately, each toggle resets the state:
return ( <div className="App"> <button type="button" onClick={handleAddUser}> Toggle </button> {show && <User name="Chimezie" />} {!show && <User name="Innocent" />} </div> );
This happens because both child components are not in the same rendering position so when you increase the age
count and then toggle to the next child component, the age
count automatically resets.
Another way to reset state in child components is by using key
props. Usually, we use the key
props to map through lists and make each list unique. Similarly, we can also use it to reset the child’s state. When the key
props of a child component change, React treats the child component as a new component, thereby resetting the state.
Using the same example above:
return ( <div className="App"> <button type="button" onClick={handleAddUser}> Toggle </button> {show ? ( <User key="Chimezie" name="Chimezie" /> ) : ( <User key="Innocent" name="Innocent" /> )} </div> );
Here, we are passing unique key
props to both child components. As the key
props changes with each toggle, the state resets with it.
When managing state in React, understanding its asynchronous nature and batching technique is crucial to optimizing performance. React batches state updates to reduce unnecessary re-renders, as seen in scenarios like resetting parent state from child components or ensuring state updates rerender components efficiently.
To prevent issues where React doesn’t re-render after state changes, avoid directly mutating objects in state and use setter functions like setState
or setCount
. Techniques like using key
props for child components or handling dependencies in useEffect
can also address challenges with parent-child state synchronization.
Adopting the practices covered in this guide ensures your React app will handle state updates efficiently, enhancing both performance and user experience.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]
2 Replies to "Why React doesn’t update state immediately"
Hi, I’m using classes, Parent and Child and trying to set a new state value after the DOM has loaded. I can see that you mention state does not update after setState() and kind of show how to get around that, but I feel your not providing a clear enough picture for us React nubes on how to force a state change to be recognized. I’ve been at for a few days and am a bit stumped. I just keep seeing the old value. Updates are firing and passing the old value back to the child class.
Hi, okay state update will not happen immediately as it is asynchronous. Now what we can do to update state immediately.