The Gist of Hooks

October 12, 2022

Many more words about React.js. Previously: The Zen of React.

As you may know, in 2018ish, React added Hooks to the library, presenting them as a new, better thing which should replace all your old class components. This was very strange and controversial at the time. Still is, judging by the comment section on Hacker News every other week. Among other things, complaints about hooks are: they’re confusing, they’re clunky, they’re unnecessary, they’re difficult to use correctly. All of these are true.

But hooks are, in fact, great. They’re the future of programming. This article, hopefully one of a series, is about getting you to agree. It’s about what hooks are and why they are how they are.


1. What are hooks?

A little summary so we’re all caught up.

A modern React class component, the thing we had before hooks, looks like this (in Typescript because we’re not barbarians):

type CounterProps = {
    initialValue: number,
    label: string;
};

type CounterState = {
    value: number;
};

class Counter extends React.Component<CounterProps, CounterState> {
    constructor(props) {
        super(props);
        this.state = {
            value: props.initialValue
        };
    }

    updateCounter() {
        this.setState({value: this.props.value + 1});
    }

    render() {
        return (
            <div class={counter}>
                <div class={button} onClick={this.updateCounter}>
                    {this.props.label}
                </div>
                {"Value = " + this.state.value}
            </div>
        );
    }

}

This defines a Counter component which, when mounted, initializes with a given value, and then each time a button on it is clicked, increments that value by one.

React promises you that the component will be updated (‘reconciled’) on screen whenever the props or state changes. Here, state changes via setState() in response to the button being pressed, and props can change whenever and it will re-render with the current value of label. Changes to initialValue happen not to do anything, because it’s only used in the constructor, but that’s totally an implementation detail of this class.

Hooks are another way of doing all of this: instead of writing classes with methods on them that React calls “in to”, write functions with hooks that call “out to” React. The same component looks like this with hooks:

const Counter: React.FC<CounterProps> = React.memo(({initialValue, label}) => {
    const [value, setValue] = React.useState(initialValue);
    const updateValue = React.useCallback(() => setValue(value => value + 1), []);
    return (
        <div class={counter}>
            <div class={button} onClick={updateValue}>
                {label}
            </div>
            {"Value = " + value}
        </div>
    );
});

It’s a bit shorter! But it’s basically totally equivalent. It’s just… weird, right?

Yeah. A bit. So why would you want to do this?

Well, basically because classes suck.


2. What’s wrong with classes?

The core idea of React is that a component will be (semi-)efficiently re-run and written to the DOM whenever the props or state change. There are also some other requirements: some escape hatches to do things before/after renders, and to jump out into regular JS that React doesn’t manage.

One obvious way to do this is have a class that takes props as arguments, and then owns its own state. So that’s what React did, and they let you update that state with a setState method that you can use wherever to trigger a re-render. There are also some lifecycle methods (componentDidUpdate, etc) that let you do the other stuff.

But it didn’t have to be this way. It’s just one implementation of the requirements of a “component”. It was the 2010s, everyone still thought object-orientation was somehow fundamental to programming, weird stuff happened, and React was built on classes. But any way of getting these requirements would do. Hooks are just another way of getting them, which turns out to be better.

The main problem with classes is just that they’re a weird meme from the 90s that never paid enough rent to stay around as long as it did. Now we basically know, intuitively, that objects should either own state or contain business logic, but probably not both. There are probably a bunch of other ways of structuring programs that didn’t win out, historically, but are just as good.

Javascript’s version of classes in particular sucks more than most (e.g. Java) , due to two obvious problems.

One is that the spread syntax {...object} doesn’t work like it should, because it only works on fields and arrow functions (defined on an object itself) but not on methods (!) or static fields, because those are defined on the prototype. This means that either spreads and classes shouldn’t exist in the same language, or, they should work together. But having them exist and also not work is just a footgun. And because of how they’re designed they’re also never going to work. (Basically they need to copy the prototype also, but it’s never going to be able to be done because it won’t be backwards-compatible with the current implementation. I cannot imagine why they did it this way.)

The other issue is, of course, the traumatic experience of this, and the need for .bind(this) all over the place if you ever actually use methods.

Presumably the reason that classes were appealing to early React is that they sorta specify a ‘type’ for a component. There’s a list of abstract methods that you can implement. Fine, another side-effect of the neverending farce that is languages without static typing.

But classes also specifically suck as a way of implementing components. Because classes provide access to the component lifecycle only through inheritance, there’s this thing I call the “Don’t Call Us, We’ll Call You” problem. It’s that, since the only way to tell React to do anything is for it to invoke your lifecycle methods on its own schedule, everything you might want to do has to live in one of the predefined locations. Even if that has nothing to do with how your component’s behaviors are logically structured.

The only real ways of re-using behavior ergonomically in class components was higher-order components (HOCs) (or equivalents, such as subclassing React.Component yourself). Neither works well: composing abstractions involves layering complexity (a(b(c(d(e))))) when all you actually need is linear complexity (a(); b(); c(); d(); e();). Both are also hell to deal with in Typescript, although that’s not, like, necessarily React’s problem.

Really seeing that hooks are good is just about letting go of the idea that there’s something fundamental about classes. It turns out that classes are just a thing someone made up and they’re pretty shitty, so one should at least entertain alternatives. And then the first time you need a component to do more than two things to manage itself and you can just call into more hooks to do it, you’ll be sold.

Like, yeah, I don’t think hooks are what programming is going to look like in fifty years, but it’s going to look more like hooks than classes. Sheesh.


3. How hooks work

It can be very instructive to see an explicit implementation of a hook written out as though it was all application code. Honestly seems like this shoudl be the first page of React docs.

Here we’ll sketch out the backend for a simple useState() hook. This example is not literally how they’re implemented, in fact, I haven’t gone to look how they’re actually implemented at all. But it is, in some sense, isomorphic to it, and it’s a plenty-good mental model for working with hooks in your day-to-day.


Hooks are implemented by setting global state before running a component and unsetting it afterwards, such that each component gets access to some local state without having to actually make a closure over it. Like so:

type HookState = {
    type: string,
    data: any,
};


type ComponentState<Props> = {
    // whether this component needs re-rendering in the next pass
    // nb: this is not how React actually does it
    dirty: boolean, 
    // a list of all the hooks this component has called in order
    hooks: HookState[],
    // whether we're presently mounting, aka being called for the first time
    isMounting: boolean,
    // the hook that's currently being executed
    currentHook: number,
}

// Let's just assume this exists in some form
let markComponentForRerender = (component: ComponentState) => {
    /* some React-provided function that we don't worry about */
}

// This iset by the renderer before a component is executed.
let currentComponent: ComponentState;

/* 
UseState:
1. Save some stuff into the hooks array,
2. Optionally mark us for re-render if needed, using a 
behind-the-scene magic React function.
*/
function useState<T>(initialValue: T) {
    const component = currentComponent;

    // first time we're invoked
    if (component.isMounting) {
        const data = [initialValue, (value) => {
            if (value !== data[0]) {
                data[0] = value;
                // rerender this component on the next render pass
                // don't worry about how this works. just trust that React does it.
                markComponentForRerender(component);
            }
        ];
        hooks.push({
            type: 'useState',
            data: data,
        });

    // every other time
    } else {
        // Do some sanity checks, aka enforce the Rule of Hooks
        assert(component.hooks.length > component.currentHook);
        const currentHook = component.hooks[component.currentHook++];
        assert(currentHook.type === 'useState');
        return currentHook.data;
    }
}

That’s how useState works, in principle.

It’s pretty easy to implement useMemo or useRef in the same way (in fact they are simpler because they can’t trigger re-renders). You can pretty much do useEffect if you assume the existence of a black-box scheduleAfterRender() function as well. Nevermind how React actually implements these signals; I’m sure it’s much more complicated due to the abstractions and performance optimizatiosn they implement. But the basic idea is quite simple. You could probably throw something together that gets most of React basicaly working without too much trouble following this pattern (the shadow DOM / reconciliation is the hard part, but you don’t actually need that to implement hooks).

The point is, hooks are not that complicated. They seem a bit strange because they’re treated as magic functions that obey strange rules, but in fact they are just regular functions with a strange calling convention. Unfortunately you don’t ever see this written out or made very explicit, so you have to imagine it yourself while you’re learning the ropes and it’s quite perplexing.


But, alas, hooks do also have problems! More on those some other time. Then maybe I’ll try to convince you that they’re good anyway, despite all the problems.


My other articles about React:

  1. The Zen of React
  2. The Gist of Hooks
  3. React Mistakes