← All writing
Craft · · 6 min

Hooks and the classes I kept

On wrapper hell in the devtools tree, why grouping by concern beats grouping by lifecycle, and the two things I'm still wary of.

React JavaScript

Hooks landed in a stable release at the start of the month. I’d watched the talk back in the fall, thought “that’s clever,” and then done nothing about it for four months. That is roughly my standard response to anything.

I’ve now built one real thing with them: a filterable directory on a client site, about 400 lines, the sort of small interactive island that’s the only React on an otherwise static build. So this is a report from two weeks rather than a considered position, and I’ve tried to keep the enthusiasm proportionate.

The short version is that the thing people are arguing about is this, and the thing hooks actually fix is something else.

The problem they were built for

The pitch that got repeated everywhere is that classes are confusing, this is confusing, and function components are simpler. Which is true as far as it goes, and it isn’t why any of this exists, and it’s a shame it became the headline because it invites a fairly boring argument.

The real problem was sharing stateful logic between components.

Say two unrelated components both need to know the window width. The state and the event listener and the cleanup are identical in both. With classes there was no way to extract that, because state lives in a class instance and you can’t lift a lifecycle method out into a shared thing. The workarounds were higher-order components and render props, and both of them work, and both of them share logic by adding a component to the tree.

Which is how you get the thing everyone recognizes when they open the devtools on a grown-up React app: 14 nested components, 11 of which render nothing at all and exist to pass something down. Your actual UI is somewhere at the bottom.

Both of the old answers shared logic by adding a component to the tree, which is why every mature React app's devtools looks like fourteen wrappers around one div.

A custom hook is just a function that calls other hooks, and it shares the logic without touching the tree at all.

use-media-query.js
function useMediaQuery(query) {
  const mq = window.matchMedia(query);
  const [matches, setMatches] = useState(mq.matches);

  useEffect(() => {
    const onChange = e => setMatches(e.matches);
    mq.addListener(onChange);
    return () => mq.removeListener(onChange);   // cleanup lives with the setup
  }, [query]);

  return matches;
}

// and then, in anything:
const isWide = useMediaQuery('(min-width: 60em)');
JavaScript

That’s the feature. Everything else is pleasant, and that’s the bit that’s structurally different from what came before.

Grouped by concern, not by lifecycle

The second thing, which I felt immediately and hadn’t anticipated.

In a class, one concern gets split across three methods. Subscribe in componentDidMount, re-subscribe in componentDidUpdate when the prop changes, unsubscribe in componentWillUnmount. And each of those methods contains fragments of three or four unrelated concerns, because that’s where the lifecycle put them. So related code is far apart and unrelated code is adjacent: the exact opposite of what you want.

useEffect inverts it. The setup and its cleanup are in the same function, and you can have as many effects as you have concerns.

The cleanup being a returned function is the detail I like most, because forgetting componentWillUnmount is one of the most common bugs I’ve written in React, and the shape of the new API makes forgetting it feel like leaving a sentence unfinished.

The two I’m wary of

The rules. Hooks are matched up by call order, so you can’t call them conditionally or in a loop, and that’s a genuinely odd constraint to bolt onto a JavaScript function. It’s not hard to follow, and the lint rule catches it, but it’s a piece of hidden state in something that looks like a plain function call, and I’ve already caught myself writing an early return above a hook without thinking.

I don’t love trading “this is confusing” for “the order of your function calls matters and nothing in the language enforces it.”

The dependency array. This is the one that will produce the bugs, and it’s already producing them in what I’ve read this two weeks.

stale.js
useEffect(() => {
  const id = setInterval(() => setCount(count + 1), 1000);
  return () => clearInterval(id);
}, []);   // count is captured once, at zero, forever
JavaScript

Nothing warns you at runtime. The counter just sticks at one and looks like a rendering problem. The empty array reads as “run this on mount,” which is how everyone translates it from the class version, and it actually means “close over these values permanently,” and those are different statements.

The lint rule that fills in the array for you is not optional as far as I’m concerned. Turn it on before you write your second component.

Does the bar come down?

This is the question that’s actually relevant to my work, since most of what I build has no React in it whatsoever and never should.

The threshold for reaching for a framework has always been about ceremony. If the thing you need is a filter on a list, a class component with a constructor, a bind in it, and three lifecycle methods is a lot of apparatus. The same thing as a function with two hooks is maybe 15 lines and reads like the plain JavaScript version with the DOM juggling deleted.

So yes, slightly. The bar has come down.

But the bar was never mostly about ceremony, it was about the bundle, and the bundle hasn’t changed. 40-odd KB of framework before your code, on a category page, so that a list can filter without a page reload, is still a bad trade most of the time, and hooks don’t touch that at all. What’s changed is that on the projects where React is already justified, the small components are now nicer to write.

The measured version

The custom hook is a real advance and solves a real structural problem that had two awkward answers before. The effect grouping is better than the lifecycle grouping and I noticed the difference within a day. The call-order rule is a wart. The dependency array will be the thing everyone gets wrong for the next two years, and the linter is doing a lot of the work.

I’m not rewriting anything. I’ve got class components in production that are fine, and “fine” has been undervalued in this industry for as long as I’ve been in it.

Read similar posts
6 min

View transitions on a multi-page site

There's one argument for building a client-side app I've never had a good answer to: a real page navigation looks worse. The answer turns out to be a browser feature rather than a rebuttal.

6 min

Intl

I added a date library out of pure habit, for one line of formatting, then removed it. Then I read what's actually built in and found four things I've been doing badly for years.