I recently saw a comment on Hacker News comparing React hooks with multiple inheritance in C++. I thought this was an interesting analogy and wanted to expand on it a bit.

This is not a hooks tutorial. I’m assuming you already know about React hooks and how to use them.

The point of this post is just to draw an interesting parallel between two different ways of solving the same problems.

Hooks are not functional

First of all, React hooks are functions, but they are not “functional.” They’re better thought of as an escape hatch for non-functional behavior in function components.

This is elaborated on further — and with considerably more editorial opinion — in the Why React is not functional post that was linked in the comment that kicked off this whole post of mine.

That post makes the common mistake of referring to function components as “functional components”, but otherwise has some good points. So I won’t belabor things here. But I did want to touch on this briefly because it illustrates why hooks exist in the first place.

Consider the following function component:

function TextInput({ value, onChange }) {
  return <input type="text" value={value} onChange={onChange} />;
}

This is a pure function:

  1. Given the same input, it always returns the same values.
  2. It has no internal state.
  3. It has no side effects.

But what if we want the function to have internal state or side effects? Well, that’s what hooks are for:

function TextInput({ defaultValue, isValid, onChange }) {
  const [value, setValue] = useState(defaultValue);
  useEffect(() => {
    if (isValid(value)) {
      onChange(value);
    }
  }, [isValid, onChange, value]);
  return <input type="text" value={value} onChange={(v) => setValue(v)} />;
}

In the above example, we have internal state via the useState hook, and side effects via the useEffect hook. Our function component isn’t so functional anymore.

In React, a function component has specific meaning — it’s a component that is a function (as opposed to a class, the original kind of React component). It’s not necessarily a pure function nor is it “functional.”

Ordinary Javascript functions can already encapsulate state (by closing over a reference) and cause side effects.

The only reason we need hooks is that the React rendering pipeline doesn’t play nice with the normal ways of doing things.

React hooks are like inheritance

This is the kinda cool part. At least, I think so!

Take a step back from React for a moment and imagine you’re writing code in an Object Oriented language like C#, Java, etc. These languages have something called inheritance, where a class can inherit functionality from another class.

What do you get when you inherit from a class?

  1. State. The inherited class can “own” some state, whether private or public. A Counter class might have a piece of state called currentValue. The state is initialized by a constructor and is associated with a particular object.
  2. Methods. The inherited class can have methods, which are just functions that encapsulate functionality associated with an instance of the class. These methods frequently read/write the object’s state.

Depending on the language, there are some other things you get as well (like “is-a” semantics when checking the object’s type, polymorphism, dynamic dispatch, etc.), but those are tangent to the point.

Now let’s look again at a React component that uses a hook:

function Counter({ initialValue }) {
  const [value, setValue] = useState(initialValue ?? 0);
  return <button onClick={() => setValue(value + 1)}>{value}</button>;
}

What is useState bringing to the table here? Even without looking at the implementation, we can infer a few things.

  1. It’s bringing state — specifically, the value variable is a piece of internal state that Counter can access. The state is initialized to a default value (which looks like a constructor if you squint), after which it’s just… there. Even if you change the initialValue prop later, the internal state will not change.
  2. It’s bringing methods, kind of. The setValue function is like a method because it exists to modify some internal state.

The parallel becomes really strong when we actually compare a useCounter hook with a Counter class:

function useCounter(initialValue) {
  const [value, setValue] = useState(initialValue ?? 0);
  const increment = () => setValue(value + 1);
  return [value, increment];
}

Versus:

class Counter {
  private value;

  constructor(initialValue) {
    this.value = initialValue ?? 0;
  }

  public increment() {
    this.value += 1;
  }

  public getValue() {
    return this.value;
  }
}

Both the class and the hook are encapsulating some state and exposing an interface that supports incrementing a value and viewing the current value.

If you think about it, this should be no surprise. React added hooks in the first place to allow function components to do the same things that class components could do (and more, but we’ll get to that in the next section!)

Multiple hooks = multiple inheritance

Normal examples of inheritance typically have one base class and multiple subclasses that inherit from it:

  • Define a class called Shape
  • Create subclasses Circle, Square, etc.

But there is actually a generalization of single inheritance that allows a single class to inherit from multiple parents, aptly named multiple inheritance.

As a side note, similar functionality can also be achieved with traits, interfaces, etc. depending on the language in question. However, one big difference is traits/interfaces are more “functional” than inheritance because they work with functions instead of objects, and therefore don’t encapsulate state.

With multiple inheritance, you can define base classes that encapsulate commonly used functionality and reuse them in your subclasses.

For example, you could define two classes:

  • A Counter class that encapsulates counter state.
  • An HttpClient class that encapsulates HTTP client behavior.

And you could use both of those in a EventTracker subclass (pseudocode):

class EventTracker extends Counter, HttpClient {
  public increment() {
    this<Counter>.increment();
    this.sendRequest('/count', this.getValue());
  }
}

Does this look familiar? In React, we could define this as a hook:

function useEventTracker() {
  const [value, increment] = useCounter();
  const { send } = useHttpClient();
  useEffect(() => {
    send(value);
  }, [send, value]);
  return increment;
}

Like, but unlike

So, are hooks “the same as” multiple inheritance? No. They solve similar problems, but there are differences.

The biggest reason for the differences is that hooks are ultimately just functions, and they are used in function components which are also just functions. In contrast, the multiple-inheritance situation is working with classes and objects, which are more complex.

All you can do with a function is just call it. It doesn’t have methods or properties. It doesn’t really have a nominative type. And, whatever functions are called within the parent function are totally hidden.

You can’t check if useEventTracker is an “instance of” useCounter like you can with the EventTracker and Counter classes.

For the pedantic among us, yes, technically Javascript functions can have methods and properties, but please, don’t do that to your hooks or function components.

This also means that polymorphism and dynamic dispatch, which are properties of objects associated with method calls (and property access, which is a similar thing), just aren’t applicable to plain functions.

Actually, this is kinda nice. Polymorphism and dynamic dispatch are powerful but also notoriously confusing in some situations.

I won’t say hooks aren’t confusing, but they manage to avoid some of the frustrating bits that come up when dealing with multiple inheritance in the wild.

Conclusion

I’m not trying to argue that hooks are good or bad, here.

They do solve a problem that’s not easy to solve with pure functions (even if it is possible), and do so without reintroducing all the pitfalls of classes and inheritance.

But on the other hand, complex usage of hooks quickly gets confusing, and they have some parts (like dependency arrays for useEffect et al.) that just feel weird to use.

I like hooks (in particular, I like how I can write custom hooks like useAppState or useApi to encapsulate app-specific behaviors). But like them or hate them, we’re stuck with them as long as we keep using React.