DEV Community

Cover image for Simplifying state management in React apps with batched updates
Brian Neville-O'Neill
Brian Neville-O'Neill

Posted on • Originally published at blog.logrocket.com on

Simplifying state management in React apps with batched updates

Written by Peter Ekene Eze✏️

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);
Enter fullscreen mode Exit fullscreen mode

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.

LogRocket Free Trial Banner

When the Component first renders, the console outputs the initial values of item1 and item2.

Initial render

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:

Initial render and additional two 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:

Initial render and only one re-render

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");
      });
    });
  }
Enter fullscreen mode Exit fullscreen mode

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 behaviour and you wouldn’t have to use the unstable API.

Should you use it?

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.

When to use it

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 });
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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 });
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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.

Does it apply to Hooks?

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" });
Enter fullscreen mode Exit fullscreen mode

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" });
Enter fullscreen mode Exit fullscreen mode

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.

Will it be deprecated?

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.

Final thoughts

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*.


Editor's note: Seeing something wrong with this post? You can find the correct version here.

Plug: LogRocket, a DVR for web apps

 
LogRocket Dashboard Free Trial Banner
 
LogRocket is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
 
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.
 
Try it for free.


The post Simplifying state management in React apps with batched updates appeared first on LogRocket Blog.

Top comments (0)