Totally in sync: Using derived state in React applications

Johannes Preis
comsystoreply

--

If you’ve written any React application that goes beyond the complexity of a basic “Hello World”, chances are you had to manage some sort of state. You might have come across recommendations on where and how to best manage your application state (e.g. by lifting state up, using Context, or even a dedicated state management library like Redux).

In this article, I want to introduce the notion of derived state. My goal is to provide you with an additional perspective for reasoning about and managing state in your applications, which can help you avoid unnecessary and error-prone synchronization of multiple dependent pieces of state while at the same time making your code more DRY and easier to understand.

Note: While the examples and recommendations in this article are tailored to React applications, the general concepts presented here are applicable regardless of the framework or library you are using.

What is derived state?

Consider the following simple shop application for ordering fruit online:

We can select from various different fruits that can be added to the cart and there’s a button to proceed to the checkout. In this basic version, the cartItems are the only piece of state and they’re used to display the contents of the cart.

While this application is quite simple, it has a flaw: We’re allowed to proceed to the checkout with an empty cart! This is easily remedied by conditionally enabling/disabling the button based on whether there are any items in the cart:

These kinds of checks are so common in our applications that we rarely give them much thought anymore. But in fact, this is the most basic form of a derived state! To make things clearer, let’s assign the result of the expression cartItems.length === 0 to a dedicated constant:

So what exactly makes isCartEmpty a derived state? In contrast to the cartItems we didn’t use a useState() hook for storing (and updating) the information on whether the cart is currently empty. Instead, this state is calculated “on the fly” based on the cart's contents. Its value is derived from the cartItems state (now referred to as base state).

Simply put, derived state is any sort of state in an application that is not updated imperatively but instead inferred (computed) from a base state and/or other derived states or parts thereof.

We can quickly see that this is a one-way road: Knowing only the value of our derived state isCartEmpty, we’re unable to determine the exact contents of cartItems. As mentioned before, we didn’t (and shouldn’t!) store this state separately using an additional useState() hook, since we never want to modify isCartEmpty manually.
Whenever the value of cartItems changes, the App component will rerender and the value for isCartEmpty will be recalculated.
As a result, we don’t have to worry about manually keeping isCartEmpty “in sync” with the current contents of the cart.

That’s the key benefit of using derived state.

To further illustrate this point, let’s take a look at how an implementation using a dedicated useState()could look like:

We’ve added an additional useState() for storing the state of isCartEmpty and use the useEffect() hook to update this state whenever cartItems changes. While this seems to work fine, there are actually two issues with this approach that are not immediately apparent:
Imagine our application would support persisting and restoring the cart of the user, so that cartItems would not be empty initially (e.g. by retrieving the contents of the cart from a backend or from localStorage). Now, the initial value for isCartEmpty (which we just hardcoded to be false) could be incorrect. We would have to do the same check (cartItems.length === 0) for properly initializing the state as well, adding unnecessary duplication to our code.

To recognize the second issue, we need to take a closer look at how useEffect() works. From the React docs:

The function passed to useEffect will run after the render is committed to the screen.

So what will happen once we add an item to cartItems? The state update will trigger a rerender of the component and only after that render the callback in useEffect will be executed. The subsequent update to the isCartEmpty state then in turn triggers another rerender. Not only is the component rerendered twice but the isCartEmpty state always “lags” behind one render cycle until it’s properly in sync with the state it depends on, which can easily lead to bugs in your application. Using derived state avoids those issues altogether.

More examples for derived state

Let’s continue tweaking our small shop application by introducing an additional feature: Instead of showing each cart item individually, we want to group the items and show the amount of each fruit in the cart. For this, we will add an additional derived state fruitCount and adapt the rendering logic for the cart accordingly:

We’re using the countBy function from ramda to dynamically build an object which tells us how much of each fruit was added to the cart, e.g.

{
"apple": 2,
"banana": 1
"kiwi": 5
}

Again, we don’t need to update this object manually — it’s recalculated every time App rerenders and any changes to cartItems will be taken into account.
Finally, to give users some incentive to buy more fruit in our shop, let’s provide them with a discount once they have three pieces of each fruit in the cart. As you might have guessed, this calls for yet another derived state isEligibleForDiscount. Since we already have our fruitCount object, checking this is very simple:

We’re using another function from ramda (all) to check that for every fruit offered in the shop (fruits), the amount of that fruit in the cart is at least three. The derived state is then used to display a notification on the checkout button. As you can see, derived state can also be calculated from another derived state. Think of a directed graph with the base state as the root node and where any changes to this base state are propagated via the links between the (derived) states.

Here’s our final application:

In our simple application, all pieces of state “live” in the same component but that’s not a necessity. The base state might just as well be passed down as a prop to various child components which could then declare individual derived states.
If you want to learn about ways to centralize your state management without resorting to a full-fledged state management library like Redux, I can recommend this article about React Context by my colleague Korbinian Schleifer.

A note on performance

As stated before, the derived states are recalculated whenever the component containing them is rerendered. Those rerenders occur whenever the state of the component or a parent component changes (for a detailed explanation on rerendering in React consider this excellent article by Felix Gerschau).
Consequently, the derived states always stay “up to date” since every change to the base state will lead to a recalculation of the derived state(s). This is what we want — but there’s a caveat: Rerenders might be triggered by other (“unrelated”) state updates in the application. So even though the base state wouldn’t change, the value for the derived state would still be recalculated. Depending on the complexity of the calculation, this can potentially degrade the performance of the application significantly, especially when many rerenders occur in a short time period (e.g. when a user is entering text in an input field).
To alleviate this problem, we need to ensure that the derived state is only recalculated if the base state it’s derived from changes. React provides the useMemo hook exactly for such use cases. From the docs:

Pass a “create” function and an array of dependencies. useMemo will only recompute the memoized value when one of the dependencies has changed. This optimization helps to avoid expensive calculations on every render.

This tells React to “memoize” the result of the calculation and only recalculate it whenever cartItems changes. Note that React uses reference equality to detect changes in the dependencies, which might be undesirable if the dependency is an object that is recreated on every render. If you want to use a deep-equality check, consider using a library like use-deep-compare-effect.

Obviously, the calculations in this example are so trivial that useMemo will provide hardly any benefit here. But even for more complex applications, I’d advise against using useMemo (or its sibling useCallback) all over the place without deliberation. The credo of performance optimization holds true here as well: Don’t optimize prematurely and if you try to optimize, measure if your optimizations actually have the desired effect.

Conclusion

In this article, we’ve explored the concept of derived state. Derived state is not updated imperatively but instead computed directly from (parts of) a base state or another derived state. The useMemo() and useCallback() hooks can be used to limit updates to the changes that are relevant for correctly updating the derived states which can help in avoiding unnecessary and potentially performance-impeding calculations.

The key point is to be conscious about the introduction of new state in your application: Always ask yourself whether it can be derived from a piece of state that’s already there. It will allow you to avoid duplications, make your code easier to reason about, and ensure that all those dependent states are kept in sync properly.

Thanks for reading!

Passion, friendship, honesty, curiosity. If this appeals to you, Comsysto may well be your future. Apply now to join us!

This blogpost is published by Comsysto Reply GmbH

--

--