Saturday 26 March 2022

Things people have trouble with in React / Javascript, part 1: Too much React state

I've been on a bit of a Stack Overflow rampage recently, most commonly swooping in and answering questions/fixing problems people are having with their React apps. I'm going to spend my next few blog posts going over what seems to be giving people the most trouble in 2022.

Episode 1: In which things are overstated

This problem typically manifests itself as a question like "why doesn't my UI update" or the inverse "my UI is always updating" (which is almost always related to useEffect - see later in this series). With the useState hook, state management becomes so easy, it's tempting to just scatter state everywhere inside a component instead of thinking about whether it belongs there, or indeed if it's needed at all.

If I see more than 3 useState hooks in one component, I get nervous, and start looking for ways to:

  • Derive the state rather than store it
  • Push it up
  • Pull it down
The React docs (and Dan Abramov himself) talk a lot about pulling-up and pushing-down state, but I think deriving state may actually be more important than either of those.

What do I mean? Well, I see people doing this:

    const [cars, setCars] = useState([]);
    const [preferredColor, setPreferredColor] = useState(undefined);
    const [preferredMaker, setPreferredMaker] = useState(undefined);
    // DON'T DO THIS:
    const [filteredCars, setFilteredCars] = useState([]);
    ...
  
Obviously I've left out tons of code where the list of cars is fetched and the UI is wired up, but honestly, you can already see the trouble brewing. The cars list and the filteredCars list are both held as React state. But filteredCars shouldn't be - it's the result of applying the user's selections (preferred color and maker) and so can be trivially calculated at render time. As soon as you realise this, all kinds of UI problems with staleness, flicker, and lag just melt away:
    const [cars, setCars] = useState([]);
    const [preferredColor, setPreferredColor] = useState(undefined);
    const [preferredMaker, setPreferredMaker= = useState(undefined);
    // Derive the visible list based on what we have and what we know
    const filteredCars = filterCars(cars, preferredColor, preferredMaker);
    ...
  

I think some people have trouble with their mental model around functional components, and are afraid to have a "naked const" sitting there in their function - somehow it's a hack or a lesser variable than one that useState handed you. Quite the reverse I think.

Another argument might be that it's "inefficient" to derive the data on each and every render. To that, I counter that if you are maintaining the cars and filteredCars lists properly (and this is certainly not guaranteed), then the number of renders should be exactly the same, and thus the amount of work being done is the same. However there's a strong chance that deriving-on-the-fly will actually save you unnecessary renders. I might keep using this car-filtering analogy through this series to explain why this is.