The Gist of Hooks

October 12, 2022

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

As you may know: in 2018ish, the React team added “hooks” to the library. From the beginning hooks have been presented as a new, better thing which would gradually take the place of class components. This was very strange and controversial at the time, and it still is, judging by the comment sections complaining about it every other week.

Common frustrations about hooks: they’re confusing, they’re clunky, they’re unnecessary, they’re difficult to use correctly. All of these are true. But I think hooks are great and that they’re the future of programming. This article, hopefully one of a series, attempts to get you to agree with that statement. It’s about what hooks are and why they are how they are.

In particular I think it really helps to see a hook written out as a simple programming exercise, in order to understand exactly how they solve the problem that they are trying to solve. When you do this, and understand what that problem is, you can see that, while hooks aren’t even a great solution to that problem, they are at least better than classes, and therefore they are a step in the right direction.


1. What are hooks?

Here’s a summary of how hooks work so we’re on the same page.

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 should 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, as code goes (certainly less so than any complex algorithm). What makes them so weird and hard-to-grok is that they are implementing a different and unfamiliar programming paradigm than what we’re used to. As a result they seem like magic functions that obey strange rules. In fact they are just regular functions with a particular calling convention that is necessary for the working of (a particular implementation of) that convention. Unfortunately you don’t ever really see this made explicit when you’re learning; if you’re lucky it starts to make a bit of sense over time.


In summary: hooks and classes are two different conventions for implementing the same basic idea of a component. Classes seem more natural because OOP is baked into our consciousness as a normal idea, to the point where classes are a language construct in JS (and they weren’t always! Actually they were added fairly recently). Underlying hooks is a claim that classes aren’t very good at this kind of thing, and hooks are a somewhat gerry-rigged implementation of a better model. Probably in the far future there’s an even better model than them, but my guess is that it is not written in JS at all, because an imperative language is not all that good for writing declarative code, which is ultimately what’s sought after here.

Perhaps you don’t care about that! Certainly there are other libraries which do a better job at staying in the imperative paradigm—which is fine if that’s your goal, and probably easier to quickly understand. But I, and evidently the React team, believe that the future is not imperative, and therefore hooks are good, and even though they’re weird for now we should start geting used to them, because the future is going to be more of that. And that’s a good thing.



My other articles about React:

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