After making an update to your component’s state using either useState
or this.setState
, parts of the component re-renders depending on the update. More so, if you have various calls to update the state within a React event handler like onClick
, React makes the updates in a batch, instead of one at a time, reducing the number of renders the component will make.
However, you might not always be making a call to update the state within an event handler and in these cases (for example within a Promise or a SetTimeout), React makes the updates synchronously instead of in a batch. This means that you’ll get multiple re-renders. Consider the example below (or check out the demo on CodeSandbox):
import React, { Fragment, useState } from "react"; import ReactDOM from "react-dom"; function Component() { const [item1, setItem1] = useState("Initial Item 1"); const [item2, setItem2] = useState("Initial Item 2"); console.log("render: ", item1, item2); function handleClickWithPromise() { Promise.resolve().then(() => { setItem1("Updated Item 1"); setItem2("Updated Item 2"); }); } function handleClickWithoutPromise() { setItem1("Updated Item 1"); setItem2("Updated Item 2"); } return ( <Fragment> <button onClick={handleClickWithPromise}> {item1} - {item2} (with promise) </button> <button onClick={handleClickWithoutPromise}> {item1} - {item2} (without promise) </button> </Fragment> ); } function App() { return <Component />; } const rootElement = document.getElementById("root"); ReactDOM.render(<App />, rootElement);
In this example, we have two state values item1
and item2
, and we update their values when any of the two buttons are clicked. However, in the first button, we make the updates within a Promise.
By default, React batches updates made in a known method like the lifecycle methods or event handlers, but doesn’t do the same when the updates are within callbacks like in SetTimeout
or Promises. This means that if you have multiple calls to update the state, React re-renders the component each time the call is made.
When the Component
first renders, the console outputs the initial values of item1
and item2
.
Then, if you click on the first button the component re-renders twice and you see the initial render and then the two subsequent re-renders:
But if you refresh the page and click on the second button, you see the initial render and only one re-render even though there are still two updates made to the state:
Why was that possible? Well, because React automatically batches updates in known methods, in this case, an event handler. Please feel free to play around with the demo to test out the functionalities yourself:
When you have multiple state calls wrapped in a Promise like in the example above, you can force React to make batched updates, hence causing only one re-render. This can be done by wrapping the calls to update the state in *ReactDOM.unstable_batchedUpdates()*
like this:
function handleClickWithPromise() { Promise.resolve().then(() => { ReactDOM.unstable_batchedUpdates(() => { setItem1("Updated Item 1"); setItem2("Updated Item 2"); }); }); }
Next, update the handleClickWithPromise()
function on the previous demo with the snippet above like we currently have on this Sandbox. Now, if you click on the first button, the updates will be batched, causing only one render. If you look at your console you should see this after you click on the first button:
This is unlike the last time where we had two re-renders on clicking the first button (with promise). As a result of wrapping the calls to update state in *ReactDOM.unstable_batchedUpdates()*
, we get the same exact functionality we had with the second button (without promise).
As I mentioned earlier, if you use the *unstable_batchedUpdates()*
keep in mind that it’s an implementation detail. Future versions of React will probably make this the default behavior and you wouldn’t have to use the unstable API.
The name of the method does make it a bit concerning whether it’s safe to use in production. However, the React team has previously encouraged (and at the time of writing, still do) the use of this API when appropriate. So, it’s safe to say that although “unstable” it is stable enough to be used in production today.
If you need to make multiple calls to update the state like in the examples above, you might find that there’s a better way to do it. In my experience, most cases where I’ve seen developers make multiple calls to update the state, those calls could have easily been replaced with a single call. Let’s take a look at some instances where you might make multiple calls to update the state.
this.setState({ ...this.state, foo: 42 }); if (condition) { this.setState({ ...this.state, isBar: true }); }
The above code could be refactored to update the state with a single call like so:
let newState = { this.state, foo: 42 }; if (condition) { newState = { ...this.state, isBar: true }; } this.setState(newState);
Of course, you are creating a whole new variable, and that is okay. Usually, as we saw earlier, React would automatically batch the updates made in certain functions and not in others. As a result, you should be deliberate about when you try to reduce the number of calls to setState
.
Another instance where you would have multiple calls to update the state is:
// Increment foo this.setState({ ...this.state, foo: this.state.foo + 1 }); this.setState({ ...this.state, foo: this.state.foo + 1 });
In this case, the subsequent calls make use of the updated values made by previous calls. Again, the above code can be refactored like so:
function incrementFooBy(delta) { return (previousState, currentProps) => { return { ...previousState, foo: previousState.foo + delta }; }; } this.setState(incrementFooBy(2));
Here, we use currying to “compose” what the update should be, based on the previous state and the intended changes and then pass the value to this.setState
.
This is probably a question you want to ask so let me stop here and say YES it does. I see this case a lot with the introduction of Hooks. Consider this example below:
const [value, updateValue] = useState({}); const [anotherValue, updateAnotherValue] = useState({}); updateValue({ content: "Hello" }); updateAnotherValue({ content: "World" });
Sometimes when you use Hooks you might find yourself creating multiple state objects in a single functional component. If you are doing this, it could be a sign that your functional component is violating the Single Responsibility Principle, doing more than one thing. If the multiple state objects make sense to belong together, then you should combine the values into one state object like so:
const [allValues, updateAllValues] = useState({}); updateAllValues({firstContent: "Hello", secondContent: "World" });
Or separate the two state objects into their own independent functional component if they don’t make sense to be together. If you don’t fall into any of the above mentioned cases, then I think you should use the *unstable_batchedUpdates*
.
I feel the need to mention that making multiple calls to update the state isn’t so bad especially because React automatically batches the updates in some cases and in other cases it doesn’t really create any performance issues. So if you find yourself needing to use *unstable_batchedUpdates*
then you must be in a very rare situation.
According to Dan Abramov’s response to a Stackoverflow question:
“However, we won’t remove [unstable_batchedUpdates
] in a minor version, so you can safely rely on it until React 17 if you need to force batching in some cases outside of React event handlers.”
And another comment he made on a Github issue goes:
“This is expected behavior because we currently only batch updates inside scopes known to React (e.g. during a synchronous lifecycle method, or during an event handler). You can work around this with unstable_batchedUpdates
as mentioned above. In the future batching will be on by default everywhere.”
As of the time of writing this article, there’s no mention in the official roadmap blog posts of any React version where the unstable_batchedUpdates
will be deprecated and there isn’t much information besides Dan’s comments that more accurately say when the API will be deprecated.
In this post, we have taken a closer look at the batched updates feature and demonstrated how it simplifies the statement management and rendering process in React applications. Having mentioned that this featured is not stable at the moment, it is worthy to note that it can be used at the moment. To get started, simply put your state calls in a callback function passed to *ReactDOM.unstable_batchedUpdates*
.
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 nowSOLID principles help us keep code flexible. In this article, we’ll examine all of those principles and their implementation using JavaScript.
JavaScript’s Date API has many limitations. Explore alternative libraries like Moment.js, date-fns, and the new Temporal API.
Explore use cases for using npm vs. npx such as long-term dependency management or temporary tasks and running packages on the fly.
Validating and auditing AI-generated code reduces code errors and ensures that code is compliant.
One Reply to "Simplifying state management in React apps with batched updates"
Lots of thanks for such an informative, useful article. Wherever you are, the best of luck to you.
Have a nice day!