Adam Innes · Blog

React 16.8 Is Out, and Hooks Are Finally Stable

· 7 min · react, javascript, hooks

Last Wednesday, February 6, the React team published React v16.8: The One With Hooks. Hooks were introduced at React Conf 2018 and have lived in alpha builds since then, and the announcement itself says the team doesn’t recommend depending on alphas in production code. Now they’re stable, so I went through the post, the Hooks docs, and the v16.8.0 release on GitHub to see what changed and what to watch for when you upgrade.

What Hooks are, according to the React team

The short definition from the announcement is that Hooks let you use state and other React features without writing a class. You can also write your own Hooks to share stateful logic between components.

The Introducing Hooks page explains why they exist, and it frames the motivation as three problems the team kept running into. The first is that reusing stateful logic is hard. React has no built in way to attach reusable behavior to a component, so people reach for render props and higher order components. Those patterns make you restructure components to use them, and the page describes the result in DevTools as a “wrapper hell” of nested layers. The docs say Hooks let you reuse stateful logic without changing your component hierarchy.

The second problem is that complex components get hard to follow because lifecycle methods mix unrelated work. Their example is data fetching split across componentDidMount and componentDidUpdate, while that same componentDidMount also sets up an event listener that gets torn down in componentWillUnmount. Hooks are meant to let you split a component into smaller functions based on which pieces are related, instead of forcing the split by lifecycle method.

The third is that classes confuse both people and machines. You have to understand how this works and remember to bind event handlers, and the team also says classes don’t minify very well and make hot reloading flaky.

Classes aren’t going anywhere

This is the part I’d want every team to read before someone opens a giant refactoring pull request. The announcement says Hooks have no breaking changes and the team has no plans to remove classes from React. The intro page goes further, saying React will keep supporting class components for the foreseeable future and that Facebook has tens of thousands of class components it has no plans to rewrite.

You can’t call Hooks inside a class, but the Hooks FAQ says you can mix classes and function components that use Hooks in the same tree. The blog post also admits Hooks don’t cover everything yet. Right now getSnapshotBeforeUpdate and componentDidCatch have no Hook equivalent, which the post calls relatively uncommon lifecycles.

Update the React packages together

The upgrade detail most likely to bite you is spelled out in the announcement: to enable Hooks, all React packages need to be 16.8.0 or higher, and Hooks won’t work if you forget to update one, with React DOM named as the example. The stable implementation covers React DOM, React DOM Server, React Test Renderer, and the shallow renderer. React Native isn’t included yet; the post says it will support Hooks in its 0.59 release.

The install command from the post bumps both core packages at once:

npm install --save react@^16.8.0 react-dom@^16.8.0

If your tests use react-test-renderer, bump it in the same change, since it was published at 16.8.0 alongside react, react-dom, react-art, and react-is. Don’t count on npm to catch a mismatch for you, either. Both react-dom 16.7.0 and 16.8.0 list their peer dependency on react as ^16.0.0 in their package metadata, so an old React DOM next to a new React satisfies the range without complaint. A 16.8.1 patch followed the same day according to the changelog, and one of its fixes is for a crash when used together with an older version of React, so the caret range above is worth keeping.

useState and useEffect

useState takes an initial value and returns a pair: the current state and a function to update it. Two details in the Hooks API Reference matter if you’re coming from classes. The setter replaces the value rather than merging objects the way this.setState does, and you can pass it a function when the next value depends on the previous one. If you set the same value again (compared with Object.is), React bails out without rendering the children or firing effects.

useEffect is where side effects go. The function you pass runs after the render is committed to the screen, and by default it runs after every completed render. If it returns a function, React treats that as cleanup. The Using the Effect Hook page explains that React runs the cleanup when the component unmounts, and also cleans up the previous render’s effect before running the next one. That second part is what fixes the classic class bug where a prop changes and you’re still subscribed to the old thing. The 16.8.0 changelog adds that an effect’s clean up has to be either undefined or a function, and null specifically isn’t allowed, so don’t write return null out of habit.

The optional second argument is an array of values the effect depends on. React compares each item with the previous render’s array and skips the effect if nothing changed. The docs warn that the array needs to include any values from the component scope that change over time and are used by the effect, or you’ll read stale values from an earlier render. Passing an empty array tells React the effect doesn’t depend on props or state, so it runs on mount and cleans up on unmount.

Here’s a small example I wrote against the 16.8 API. It’s a custom Hook that tracks whether the browser tab is visible, and a component that only runs a timer while it is:

import React, { useState, useEffect } from 'react';

function usePageVisible() {
  const [visible, setVisible] = useState(!document.hidden);

  useEffect(() => {
    function handleChange() {
      setVisible(!document.hidden);
    }
    document.addEventListener('visibilitychange', handleChange);
    return () => {
      document.removeEventListener('visibilitychange', handleChange);
    };
  }, []);

  return visible;
}

function RefreshCounter({ intervalMs }) {
  const visible = usePageVisible();
  const [ticks, setTicks] = useState(0);

  useEffect(() => {
    if (!visible) {
      return;
    }
    const id = setInterval(() => setTicks(t => t + 1), intervalMs);
    return () => clearInterval(id);
  }, [visible, intervalMs]);

  return <p>Refreshed {ticks} times{visible ? '' : ' (paused)'}</p>;
}

The listener effect follows the same shape the FAQ uses for a window event listener: subscribe inside the effect, unsubscribe in the cleanup, empty array. The timer effect lists visible and intervalMs because it reads both. When the tab is hidden or the interval prop changes, React clears the old interval before the effect runs again, and the functional update means the timer never reads a stale ticks. The docs define a custom Hook as a function whose name starts with use and that calls other Hooks, and that prefix is what the lint rule looks for.

useContext and useRef

useContext takes a context object from React.createContext and returns the current value from the nearest provider, re-rendering the component when that provider updates. The overview describes it as a way to subscribe to context without introducing nesting:

function SaveButton() {
  const theme = useContext(ThemeContext);
  return <button className={theme.buttonClass}>Save</button>;
}

useRef returns a mutable object whose .current property starts at the value you pass and persists for the full lifetime of the component. The docs point to accessing a child imperatively as a common use, like holding an input’s DOM node so you can call focus() on it.

The Rules of Hooks

The Rules of Hooks page boils it down to two rules. Only call Hooks at the top level, never inside loops, conditions, or nested functions. And only call Hooks from React function components or from custom Hooks, not from regular JavaScript functions.

React relies on the order in which Hooks are called to match each useState and useEffect call with its stored state between renders. Wrap one in an if that flips between renders and every Hook after it lines up with the wrong data. If you need an effect to run conditionally, put the condition inside the effect instead.

The team ships an ESLint plugin, eslint-plugin-react-hooks, to enforce both rules, and the announcement strongly recommends enabling it. Per the plugin README, you install it as a dev dependency, add react-hooks to your plugins, and turn on its one rule:

{
  "plugins": ["react-hooks"],
  "rules": {
    "react-hooks/rules-of-hooks": "error"
  }
}

The FAQ says the rule treats any function starting with use and a capital letter as a Hook, expects Hook calls inside a PascalCase component or another useSomething function, and admits the heuristic can produce false positives. If you’re on Create React App, the README says to wait for a react-scripts release that includes the rule rather than adding it yourself.

How I’d start

My advice is to follow the team’s lead and not rewrite anything that works. The announcement recommends trying Hooks in some new components, and the intro page suggests starting with new and non-critical components and making sure everyone on the team is comfortable first. A small leaf component with a bit of state and one effect is a good first candidate, and the lint rule should go in the same pull request so the rules are enforced from day one.

For tests, 16.8.0 adds ReactTestUtils.act(). The blog post recommends wrapping rendering and updates in it so tests behave more like the browser, and it flushes effects too.

Hooks are a big shift in how React components can be written, but nothing about this release forces your hand. Upgrade every React package to 16.8 together, turn on the lint rule, and let new code be where you learn them.

← all posts