Little React Things: Less reacting, more deriving

In this second post in the Little React Things series, I talk about how we should remove unnecessary reactions from our React applications and replace them with derivations.

Back when React Hooks first came out, the useEffect was introduced and many like myself were like "great, a utility to react to things changing and keeping things in sync". And that is partially true, but it's not as simple as that. The new React docs that are under construction are more explicit about how and how not to use useEffect in the You Might Not Need an Effect.

Even with the guidance from the new docs, these useEffects still tend to pop up in the wild. They can be more than just unnecessary, and actually problems that should be fixed.

A hasty reaction

Let's say we have an app where a user can buy clothes, but instead of picking out each individual article of clothing, they can pick a bunch of articles of clothing that they like and set a price. Then the application shows the user a bunch of outfits that fit the chosen parameters.

The parent component of the app is where the user can pick the different pieces of clothing from the main list. Then there is a child component, OutfitList that lists all the outfits that fit the parameters. This list of outfits is generated by a very cool helper function called createOutfits; it's not an outrageously expensive function, but we definitely don't want to call it each time the OutfitList is rendered (this kind of function would not be done in the browser typically, but this is a React post, so here we are).

Let's take a look at a way we could create this OutfitList component.

function OutfitList({ clothingItems, maxPrice }) {
  const [outfits, setOutfits] = useState(null)

  useEffect(() => {
    const createdOutfits = createOutfits(clothingItems, maxPrice)
    setOutfits(createdOutfits)
  }, [clothingItems, maxPrice])

  return (
    <div>
      {outfits ? outfits.map(outfit => (
        <Outfit key={outfit.id} outfit={outfit} />
      )) : null}
    </div>
  )
}

It's great right? We have a useEffect set up to react to changes in the clothing item list or price limit and recreates the list of outfits and updates the outfits state. We only call the createOutfits function when we need to and we keep outfits in sync. All good? Well, there are a couple of issues with this solution.

Reactions are usually not a good idea

Hold up, but it's called "React", what do you mean reactions aren't a good idea? Well, the reacting that we do want is reacting to user actions primarily and also asynchronous events. Code that synchronously reacts to your own code, on the other hand, is not desirable.

When the OutfitList first renders, even if clothingItems is already populated outfits will still be null. This is because useEffect runs after the render. So, for the first render we have populated clothingItems and null outfits. The state is out of sync for that first render which is no good. Our components then need to be made in such a way to account for these discrepancies. For example, we need to add a check to see if outfits is null as shown in the code above.

Furthermore, in the last post, I gave the guidance to not call the function that is a React application more than you need to, and here the useEffect is calling that function immediately after it is first called. So instead of one render, we will always have at least two renders each time either outfits or maxPrice changes.

State should be the source of truth

For our OutfitList component, let's assume that clothingItems and maxPrice are state in the parent component. Given that, we have three states: clothingItems, maxPrice and outfits. But are those three states actually representing different things? I would argue no. outfits is just a derivation of clothingItems and maxPrice.

This isn't good, state should be a source of truth. But here we have clothingItems, maxPrice, and outfits as state, so which is the source of truth? From the user's perspective, I would probably say it's clothingItems and maxPrice.

Why is this important? Well, what if down the road a developer is going to add a feature where the user can add a new outfit to the outfit list. The correct way to do this would be suggest some outfits that include a piece that weren't in the clothingItems list or that was a slightly higher price than maxPrice and then if the user picks one of those outfits, adjust those states accordingly. But this developer could see some state called outfits and simply just append a new outfit to that list. Uh oh, now we're really out of sync. We have a list of outfits that could not be created from our set of clothingItems or that is a higher price than the price limit. This example is a little contrived, but I hope you get the point at least.

Deriving forward

function OutfitList({ clothingItems, maxPrice }) {
  const outfits = useMemo(() => {
    return createOutfits(clothingItems, maxPrice)
  }, [clothingItems, maxPrice])
	
  return (
    <div>
      {outfits.map(outfit => (
        <Outfit key={outfit.id} outfit={outfit} />
      )}
    </div>
  )
}

Here we are being explicit that outfits are derived from clothingItems and maxPrice. On the first render, outfits will have a value (given that clothingItems and maxPrice have values of course), we aren't immediately re-rendering after the first render, and we are still only calling createOutfits when clothingItems or maxPrice changes thanks to useMemo.

So there you have it, with one technique we followed all three guidelines from the last post, we reduced the number of function calls (renders), simplified the parameters (state), and used memoization. Until next time!