Brief Reactions to React Hooks

At ReactConf 2018, the React team introduced a new set of APIs called “Hooks” that allow you to write large swaths of traditional class-based React components as functional components.

My personal TL;DR from React Hooks is that functional components now have first-class access to all of the existing React features. Local state, Contexts, Refs, and Component Lifecycle can all now be utilized by functional components, which is awesome! 😄

It’s pretty clear that an enormous amount of time and effort was put into making these new APIs feel “correct” in relation to the existing React APIs.

That being said, I had a couple thoughts after working with the new APIs for a couple days:

setState Argument confusion

setState is the Hook that creates local component state variables. The first example presented in the Hooks documentation for this is as follows:

import { useState } from 'react';

function Example() {
  // Declare a new state variable, which we'll call "count"
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

Simple enough. Once you’ve seen Hooks being used a couple times, it’s super clear what this is doing. However, I’m still not 100% sold on the call signature of useState itself. Here’s my main concern: it isn’t immediately obvious from the function name/context that we are passing in an initial state value.

Looking at the next given example makes my concern clearer:

// Declare multiple state variables!
const [age, setAge] = useState(42);
const [fruit, setFruit] = useState('banana');
const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);

If I were blind to the API documentation of useState, I don’t think I’d really have any idea why 42, 'banana' and [{ text: 'Learn Hooks' }] are all valid arguments to useState. At first pass, my assumption would be that we’re calling useState with some type of symbolic identifier that makes sure we’re getting the “right” state variable.

Of course, knowing the API signature, I think that the way useState is set up makes a good compromise — React needs some way to know the initial value of a state variable, and passing this to useState as an argument is a logical way of doing so. I’m slightly tepid because I feel there is a conceptual muddle in mixing state declaration and initial state.

I’m also not a fan of how useState distributes initial state in various parts of the component — instead of having all initial state be in the constructor or static state variable. As I’ll discuss later, Hooks are required to be called at the top-level. I’d also suggest that, when possible/practical, Hooks be called at the top of components.

On the other hand, I’m really positive on the use of array decomposition to receive the state value and setter. That’s a super intuitive part of the API, and effortlessly colocates both declarations that are relevant for any particular state variable.

On “The Rules of Hooks”

Internally, React relies on the order by which you call hooks. This is how React determines for each call to, for example, useState which value corresponds to which state variable. This introduces a set of invariants that are pushed on to the consumer of the Hooks APIs — the “rules of Hooks”.

Form an abstract “deontological” perspective, I’m skeptical of APIs that enforce un-idiomatic and non-structural constraints. Take this example:

// 🔴 We're breaking the first rule by using a Hook in a condition
if (name !== '') {
useEffect(function persistForm() {
  localStorage.setItem('formData', name);
});
}

While I think these restrictions can be learned fairly easily, it does add some cognitive load.

My skepticism aside, I think that these changes can be rolled out safely with effective messaging. The fact that there’s already an ESLint plugin for the “rules of hooks” bodes well.

I also hope that, like many of Reacts other features, Hooks has good internal sanity checks and error reporting. For example, React should be able to recognize if the number of calls to Hooks differs between renders, which would indicate incorrect usage of Hooks.

Context consumption is vastly improved

On a very positive note, I’m a really big fan of how Hooks changes the way React Contexts are consumed. Consider the following Class component, where we want to consume 2 contexts:

render() {
  <ThemeContext.Consumer>
    {theme => (
      <DataContext.Consumer>
        {data => (
          <div className={theme}>{data}</div>
        )}
      </DataContext.Consumer>
    )}
  </ThemeContext.Consumer>
}

By the time we get to actually rendering the parts of the component we care about (the inner div), we’re 4-5 indents in — never mind all the visual noise introduced by having to write context consumers as JSX-returning functions.

This can be condensed significantly with the useContext hook:

function ContextConsumerExample() {
  const theme = useContext(ThemeContext);
  const data = useContext(DataContext);

  return <div className={theme}>{data}</div>
}

As Ryan Florence pointed out during his ReactConf talk, the useContext hook removes the false hierarchy suggested by the old Context.Consumer model.

I almost never used React Contexts before Hooks, but now I can see myself using them more. It feels like less of a commitment to consume a context — because with Hooks it’s just a variable that you pull out of React, instead of dictating the structure of your JSX.

Conclusions

I hope that Hooks gets adopted into React Core. People more versed in API-design and frontend tooling than I have already come up with some interesting proposals in the public RFC that could smooth over some of the rough parts of the API.

React feels at its best when you can declaratively describe components using functional API primitives, and I’d argue that Hooks gives the community a lot of extra room to increase expressivity while maintaining readability.

Bravo 👏

More on Hooks…