React pitfalls 1: Don't use useEffect for derived state

Deriving state from some other state is a common requirement when building user interfaces. In React, I’ve often seen derived state being implemented by using an effect. This is a short post on why using effects to derive state is a bad idea.

What is derived state?

Imagine we’re building an online shop and want to show a lists of products. We also want to give the user an option to sort the products, e.g. by price or name. That means, we need to build a sorted list of products based on the original product list and the selected sort order. That makes the sorted list a state that’s derived from some other state, in this case the product list and the sort order. In other words, the sorted list is a derived state.

The double render problem

At first glance, deriving state looks like another nail, for the hammers that are useState and useEffect. We have three states, the original product list, the sort order and the sorted product list. Whenever, either the original product list or the sort order changes, we want to update the sorted list accordingly. In React, this could be implemented as:

function ProductList ({products}: {products: Product[]}) {
  const [sortOrder, setSortOrder] = useState<"name" | "price">("name")
  const [sortedProducts, setSortedProducts] = useState<Product[]>([])

  useEffect(() => {
    setSortedProducts(sortBy(products, sortOrder))
  }, [products, sortOrder])

  return (
    // render product list
  )
}

The problem with the above solution is that it causes superfluous React renders. Here is an example: We cause a first render by updating to products or sortOrder. After the first render completes, the effect runs and we update the sorted product list with setSortedProducts. This causes a second render. As a result, our app is doing more work than necessary. We had all information we needed to derive sortedProducts during the first render already, so there was no need to cause a second render. Here are some screenshots from the React DevTools and some console logs for when changing the sort order once:

The solution

To fix the double-render issue, we can derive the state during the initial render. If the state derivation is expensive, we want to use useMemo to only re-derive the state whenever a relevant input state changes, instead of every time the component renders. Here is the fixed version:

function ProductList ({products}: {products: Product[]}) {
  const [sortOrder, setSortOrder] = useState<"name" | "price">("name")

  const sortedProducts = useMemo(
    () => sortBy(products, sortOrder), 
    [products, sortOrder]),
  )

  return (
    // render product list
  )
}

The above code not only fixes the double-rendering issue, but is also shorter and arguably more readable. Instead of making the reader worry about potential side-effects, we communicate to the reader that the state-derivation is pure. Here are the screenshots to confirm that we are indeed only rendering once where we rendered twice before:

Deriving state can come in many forms and is not always as obvious as in the above example. To help yourself avoid that issue, anytime you set state inside an effect, ask yourself: Is that state I’m setting here derived and can I remove this effect.

© 2022 Julian Vossen