PostHole
Compose Login
You are browsing us.zone2 in read-only mode. Log in to participate.
rss-bridge 2018-12-13T00:00:00+00:00

Why Do React Hooks Rely on Call Order?

Lessons learned from mixins, render props, HOCs, and classes.


Why Do React Hooks Rely on Call Order?

December 13, 2018

Pay what you like

At React Conf 2018, the React team presented the Hooks proposal.

If you’d like to understand what Hooks are and what problems they solve, check out our talks introducing them and my follow-up article addressing common misconceptions.

Chances are you won’t like Hooks at first:

They’re like a music record that grows on you only after a few good listens:

[Positive HN comment from the same person four days later]

When you read the docs, don’t miss the most important page about building your own Hooks! Too many people get fixated on some part of our messaging they disagree with (e.g. that learning classes is difficult) and miss the bigger picture behind Hooks. And the bigger picture is that Hooks are like functional mixins that let you create and compose your own abstractions.

Hooks are influenced by some prior art but I haven’t seen anything quite like them until Sebastian shared his idea with the team. Unfortunately, it’s easy to overlook the connection between the specific API choices and the valuable properties unlocked by this design. With this post I hope to help more people understand the rationale for the most controversial aspect of Hooks proposal.

The rest of this post assumes you know the useState() Hook API and how to write a custom Hook. If you don’t, check out the earlier links. Also, keep in mind Hooks are experimental and you don’t have to learn them right now!

(Disclaimer: this is a personal post and doesn’t necessarily reflect the opinions of the React team. It’s large, the topic is complex, and I may have made mistakes somewhere.)


The first and probably the biggest shock when you learn about Hooks is that they rely on persistent call index between re-renders. This has some implications.

This decision is obviously controversial. This is why, against our principles, we only published this proposal after we felt the documentation and talks describe it well enough for people to give it a fair chance.

If you’re concerned about some aspects of the Hooks API design, I encourage you to read Sebastian’s full response to the 1,000+ comment RFC discussion. It is thorough but also quite dense. I could probably turn every paragraph of this comment into its own blog post. (In fact, I already did that once!)

There is one specific part that I’d like to focus on today. As you may recall, each Hook can be used in a component more than once. For example, we can declare multiple state variables by calling useState() repeatedly:

function Form() {
const [name, setName] = useState('Mary');              // State variable 1
const [surname, setSurname] = useState('Poppins');     // State variable 2
const [width, setWidth] = useState(window.innerWidth); // State variable 3

useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
});

function handleNameChange(e) {
setName(e.target.value);

function handleSurnameChange(e) {
setSurname(e.target.value);

return (
<input value={name} onChange={handleNameChange} />
<input value={surname} onChange={handleSurnameChange} />
<p>Hello, {name} {surname}</p>
<p>Window width: {width}</p>
</>

Note that we use array destructuring syntax to name useState() state variables but these names are not passed to React. Instead, in this example React treats name as “the first state variable”, surname as “the second state variable”, and so on. Their call index is what gives them a stable identity between re-renders. This mental model is well-described in this article.

On a surface level, relying on the call index just feels wrong. A gut feeling is a useful signal but it can be misleading — especially if we haven’t fully internalized the problem we’re solving. In this post, I’ll take a few commonly suggested alternative designs for Hooks and show where they break down.


This post won’t be exhaustive. Depending on how granular you’re counting, we’ve seen from a dozen to hundreds of different alternative proposals. We’ve also been thinking about alternative component APIs for the last five years.

Blog posts like this are tricky because even if you cover a hundred alternatives, somebody can tweak one and say: “Ha, you didn’t think of that!”

In practice, different alternative proposals tend to overlap in their downsides. Rather than enumerate all the suggested APIs (which would take me months), I’ll demonstrate the most common flaws with specific examples. Categorizing other possible APIs by these problems could be an exercise to the reader. 🧐

That is not to say that Hooks are flawless. But once you get familiar with the flaws of other solutions, you might find that the Hooks design makes some sense.


Flaw #1: Can’t Extract a Custom Hook

Surprisingly, many alternative proposals don’t allow custom Hooks at all. Perhaps we didn’t emphasize custom Hooks enough in the “motivation” docs. It’s difficult to do until the primitives are well-understood. So it’s a chicken-and-egg problem. But custom Hooks are largely the point of the proposal.

For example, an alternative banned multiple useState() calls in a component. You’d keep state in one object. That works for classes, right?

function Form() {
const [state, setState] = useState({
name: 'Mary',
surname: 'Poppins',
width: window.innerWidth,
});
// ...

To be clear, Hooks do allow this style. You don’t have to split your state into a bunch of state variables (see our recommendations in the FAQ).

But the point of supporting multiple useState() calls is so that you can extract parts of stateful logic (state + effects) out of your components into custom Hooks which can also independently use local state and effects:

function Form() {
// Declare some state variables directly in component body
const [name, setName] = useState('Mary');
const [surname, setSurname] = useState('Poppins');

// We moved some state and effects into a custom Hook
const width = useWindowWidth();
// ...

function useWindowWidth() {
// Declare some state and effects in a custom Hook
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
// ...
});
return width;

If you only allow one useState() call per component, you lose the ability of custom Hooks to introduce local state. Which is the point of custom Hooks.

Flaw #2: Name Clashes

One common suggestion is to let useState() accept a key argument (e.g. a string) that uniquely identifies a particular state variable within a component.

There are a few variations on this idea, but they roughly look like this:

// ⚠️ This is NOT the React Hooks API
function Form() {
// We pass some kind of state key to useState()
const [name, setName] = useState('name');
const [surname, setSurname] = useState('surname');
const [width, setWidth] = useState('width');
// ...

[...]


Original source

Reply