By now, informed reader, you’ve surely glanced at, skimmed through, or at least bookmarked half a dozen articles about React 16.8’s most hotly anticipated feature: hooks. You’ve likely read or heard about how great they are, how terrible they are, and maybe even how confusing they are. You might be asking yourself “why should I learn this?” and you’re probably hoping for a better answer than “because it’s the new thing”. If you went so far as to follow along a few hooks guides, you might’ve found yourself asking “but why? I can do the same thing using classes!”
Credit @lizandmollie
If this sounds familiar, it’s probably because it’s the same cycle we go through every time we’re faced with having to learn a new thing. Learning new things can be difficult for anyone, and relearning something you already know can be especially frustrating. Your instinctual reaction might be to frame new things in terms of what you already know. When I first shared my learnings about hooks to my team at OkCupid, I made a chart mapping component lifecycle methods to hook alternatives, which left me looking a little like this guy:
Author’s note: apologies to the OkCupid web team for being my guinea pigs.
It turns out, this can be a very confusing way to learn hooks! A lot of concepts don’t map well, or seem unnecessarily more complicated in one approach versus another. Rather than continuing to talk about my failures, I’ll get to the good stuff. This isn’t intended to be a comprehensive guide to everything about hooks, but I hope once you’ve finished reading, you’ll feel interested enough to want to write your first component with hooks. In my experience, that’s the real secret: it won’t necessarily click until you start to write them for yourself. Without further ado, this is the single best* approach to learning hooks known to mankind**.
*I mean, it’s okay
**that I, personally, have found by publish time
Tired: setState; Wired: useState
We’ll start with the basics. You might’ve already seen this hook explained, and if so, feel free to skip to the next section, where we start to get more in depth.
One of the first things we learn to do in React is to make a stateful component. You, like me, learned to write a component by extending React.Component
(or more probably, using React.createClass
, but we don’t talk about those dark days). You learned to use this.setState({ someKey: someValue })
to modify the component’s state, remembering that the key/value pairs you pass into setState
overwrite the old state with your new values, and everything else gets merged in. Oh, and without forgetting to initialize the state object so we don't get errors when we try to setState
. And of course, we musn't forget to .bind
every function that’s going to be modifying state in the constructor, or alternatively remember to use the arrow function syntax someone on your team installed a babel plugin for a few years ago.
Let’s forget about all of that for a second. Let’s outline what we’d need to build, say, a simple, stateful click counter component:
- We need to know the current click count. Let’s call this
currentCount
. - We need a way to increment the click count. Let’s call this
setCurrentCount
.
If we imagine for a second we have these prerequisites, we might write something like this:
// Counter.jsx
import React from "react";
const Counter = () => {
return (
<div>
Current count: {currentCount}
<button onClick={() => setCurrentCount(currentCount + 1)}>
Increment
</button>
</div>
);
};
Ordinarily we’d probably reach for setState to turn this into a reality, which would mean having to refactor this tiny functional component into a full-blown class component. But hang on—this is where our first hook comes in.
// Counter.jsx
import React, { useState } from "react";
const Counter = () => {
const [currentCount, setCurrentCount] = useState(0);
return (
<div>
Current count: {currentCount}
<button onClick={() => setCurrentCount(currentCount + 1)}>
Increment
</button>
</div>
);
};
“Um, what the heck just happened?!” you might be asking yourself. Chill, self. This is a hook! Hooks allow functional components to hook into features previously only available to class components, such as state.
The useState
hook is a function that takes in a single argument: the initial state (in this case, 0
) and returns to you the current value and a setter for that value wrapped in an array, in that order. When you call the setter, React re-renders the component with your updated state value, just as it would if you’d called setState
.
“Why array destructuring?”, you ask? Well, this way you can name the value and setter whatever the heck you’d like. And of course, you can use the useState
hook as many times as you’d like within your component, so that you can keep track of multiple pieces of state if you need to without having to convert your state representation into an object. We’ll learn more about the opportunities this affords us in the next section.
Tired: a single object to hold state; Wired: separate state for separate concerns.
One of the neat things about useState
is your component’s state representation doesn’t have to be an object—it can be a number, a string, or really anything you’d like (including an object). But what does this mean for adding new state properties? Let’s say you later decide you need to keep track of another stateful property. When state was an object, this was as easy as adding another key. Now, it’s as simple as adding another call to useState
:
// Counter.jsx
import React, { useState } from "react";
const Counter = () => {
const [currentCount, setCurrentCount] = useState(0);
const [isClicking, setIsClicking] = useState(false);
return (
<div>
Current count: {currentCount}
Is clicking: {isClicking}
<button
onClick={() => setCurrentCount(currentCount + 1)}
onMouseDown={() => setIsClicking(true)}
onMouseUp{() => setIsClicking(false)}>
Increment
</button>
</div>
);
};
This also grants us the flexibility of letting us group related chunks of code together, rather than grouping potentially unrelated state changes into one setState
call. For example, if I wanted to move some of the event handlers out of the return block, I could group with with the most appropriate code like so:
// Counter.jsx
import React, { useState } from "react";
const Counter = () => {
const [currentCount, setCurrentCount] = useState(0);
const incrementCounter = () => setCurrentCount(currentCount + 1);
const [isClicking, setIsClicking] = useState(false);
const onMouseDown = () => setIsClicking(true);
const onMouseUp = () => setIsClicking(false)
return (
<div>
Current count: {currentCount}
Is clicking: {isClicking}
<button
onClick={incrementCounter}
onMouseDown={onMouseDown}
onMouseUp{onMouseUp}>
Increment
</button>
</div>
);
};
Cool, right? In a traditional class component, the these state properties would have to live together in a single object, and the initialization of the state and the functions for modifying it would likely be spread across your component, rather than grouped together with related logic. For a component this simple, the benefits might be minor, but for larger components, it can really make a difference to the readability of your component.
Tired: lifecycle methods; Wired: data that changes when you need it to change.
So we’ve learned useState
can take the place of (and in some ways improve upon) setState
. But what about all the other powerful things we can do with lifecycle methods in class components? Here’s where things can get a little hairy for experienced React developers learning hooks.
Lifecycle methods are an abstraction that make us think in terms of what stage of rendering a component we’re in. Names like componentDidMount
, componentDidUpdate
, and componentWillUnmount
feel intuitive to those of us that have been using them for years and reliably know exactly when and why they’ll run—something that can be very confusing for beginners to learn. That said, in my experience, I’ve found we usually use them in a few predictable patterns. Raise your hand if any of these sound familiar:
componentDidMount
andcomponentWillUnmount
for attaching/removing event listeners, or setting/clearing a timeout. (Example: listening to document scroll or keypress events to change state)componentDidMount
andcomponentDidUpdate
for loading something based on a prop/state change. (Example: loading data when we land on a page, and reloading when state changes)componentDidMount
andcomponentDidUpdate
for recalculating some DOM property based on a prop/state change. (Example: scrolling to the top of an element after a state change)
Often, we’re using several of these patterns at once, and oh can those lifecycle methods get messy. Related logic is by necessity spread across several of these methods, and can be hard to follow what’s happening in the kerfuffle. But it doesn’t have to be this way! Fundamentally, most of these patterns can be simplified to: do something when something happens. And thankfully, we’ve got a few hooks that can help with that.
Let’s say we want to get rid of the button in our counter component, and instead just listen to clicks on the document. Rather than writing lifecycle methods to setup and tear down those event handlers, let’s use a new hook called useEffect
.
// Counter.jsx
import React, { useState, useEffect } from "react";
const Counter = () => {
const [currentCount, setCurrentCount] = useState(0);
const incrementCounter = () => setCurrentCount(currentCount + 1);
useEffect(() => {
document.addEventListener("click", incrementCounter);
return () => {
document.removeEventListener("click", incrementCounter);
};
}, [incrementCounter]);
const [isClicking, setIsClicking] = useState(false);
const onMouseDown = () => setIsClicking(true);
const onMouseUp = () => setIsClicking(false);
useEffect(() => {
document.addEventListener("mousedown", onMouseDown);
document.addEventListener("mouseup", onMouseUp);
return () => {
document.removeEventListener("mousedown", onMouseDown);
document.removeEventListener("mouseup", onMouseUp);
};
}, [onMouseDown, onMouseUp]);
return (
<div>
Current count: {currentCount}
Is clicking: {isClicking}
</div>
);
};
Woah, that was a lot. Let’s focus on one of these new useEffect blocks.
useEffect(() => {
document.addEventListener("click", incrementCounter);
return () => {
document.removeEventListener("click", incrementCounter);
};
});
This neat little hook can be tricky to understand—it might be easier with good old-fashioned named functions (I miss those):
useEffect(function setUp() {
document.addEventListener("click", incrementCounter);
return function tearDown() {
document.removeEventListener("click", incrementCounter);
};
});
That’s better. Essentially, we’re telling the component to run our setUp() function after it renders, and to clean up after itself using the tearDown function, before the next render. For example, if this component were to re-render three times, it would run the code in useEffect
as follows:
- render
- setUp()
- render (x2)
- tearDown()
- setUp()
- render (x3)
- tearDown()
- setUp()
…and so on. However, for the sake of efficiency, we can optionally pass useEffect
a second parameter:
useEffect(function setUp() {
document.addEventListener("click", incrementCounter);
return function tearDown() {
document.removeEventListener("click", incrementCounter);
};
}, [incrementCounter]);
This second parameter is a list of items that should cause the component to re-run the setUp
and tearDown
functions. Usually this list will include the external variables you reference within the useEffect call—in this case, incrementCounter
. This lets us avoid wasteful setups and teardowns. Even more powerfully, it can help us prevent the effect from running more than once, simply by passing it an empty list of values to change on. Helpful!
In the above example, we useEffect
to setup some document event listeners every time incrementCounter
changes, but we can use this same hook to run any sort of side effect we’d like—from subscribing and unsubscribing to a web socket, hitting an API for updated data when some prop changes, or any other prop or state driven action we might want to take.
It’s worth noting at this point that the tearDown
return value is completely optional. We don’t always have something we want to clean up, but sometimes we do and now we can keep that related logic together. It can also be a helpful reminder to cleanup after our side effects, where we might have previously forgotten to cleanup in a componentWillUnmount
.
Tired: component methods; Wired: useCallback.
“But wait!”, you might be saying right about now, “we defined incrementCounter in a render function! That’ll be redefined on every render, so won’t our hook run every time?” Well well well, I didn’t realize you were paying such close attention to this overly-stretched-thin example component. But you’re absolutely right!
// Counter.jsx
import React, { useState, useEffect } from "react";
const Counter = () => {
const [currentCount, setCurrentCount] = useState(0);
const incrementCounter = () => setCurrentCount(currentCount + 1);
useEffect(() => {
document.addEventListener("click", incrementCounter);
return () => {
document.removeEventListener("click", incrementCounter);
};
}, [incrementCounter]);
return (
<div>
Current count: {currentCount}
</div>
);
};
Because incrementCounter
is being redefined on every render, using it in the second parameter of useEffect
doesn’t really give us much benefit. Thankfully, there’s a hook for that!
// Counter.jsx
import React, { useState, useEffect, useCallback } from "react";
const Counter = () => {
const [currentCount, setCurrentCount] = useState(0);
const incrementCounter = useCallback(
() => setCurrentCount(currentCount + 1),
[setCurrentCount, currentCount],
);
useEffect(() => {
document.addEventListener("click", incrementCounter);
return () => {
document.removeEventListener("click", incrementCounter);
};
}, [incrementCounter]);
return (
<div>
Current count: {currentCount}
</div>
);
};
This is one of my favorite, and most generally useful, hooks—even if you write off the entire concept of hooks, you’ll wanna keep this one in your tool belt. You give useCallback
a function as its first parameter, and it returns a memoized version of it, which only recalculates whenever any of the items in the second parameter change.
This is especially useful because we all know passing arrow functions down as props is no bueno, as this can cause wasteful rerenders. Now, fixing that is as easy as wrapping your arrow function in a useCallback
hook! It’s an easy way to squeeze out some improved performance, and prevent unnecessary rerendering.
Tired: reselect; Wired: useMemo.
I’d be remiss at this point if I didn’t talk about a close cousin of useCallback
, the amazingly useful useMemo
hook. Use this hook any time you find yourself calculating something expensive in a render block. For example, if your component body looks something like this:
// MyContrivedComponent.jsx
import React from "react";
const MyContrivedComponent = ({ someObject }) => {
const someNumber = Object.keys(someObject)
.map((key) => someObject[value])
.filter((value) => value % 2 === 0)
.reduce((sum, current) => sum + current, 0)
const array = [...new Array(someNumber)];
return (
<div>
{array.map(() => <span />)}
</div>
);
};
(This is a ridiculously contrived example, of course, but tell me with a straight face you don’t have something somewhere in your codebase that looks like that and I’ll eat my hat.) You might instead wrap this expensive calculation in useMemo
like so:
// MyContrivedComponent.jsx
import React, { useMemo } from "react";
const MyContrivedComponent = ({ someObject }) => {
const array = useMemo(() => {
const someNumber = Object.keys(someObject)
.map((key) => someObject[value])
.filter((value) => value % 2 === 0)
.reduce((sum, current) => sum + current, 0)
return [...new Array(someNumber)];
}, [someObject]);
return (
<div>
{array.map(() => <span />)}
</div>
);
};
Now, that array will only recalculate itself if the value of someObject
changes. This is much more efficient than recalculating it on every render (though still admittedly less efficient than deleting it entirely because it’s Very Bad™️). In the past, libraries like reselect gave us tools for getting similar performance benefits, but now you can reap these gains without having to import an additional library.
Tired: higher order components / mixins; Wired: custom hooks.
One more thing. Going back to our Counter
example (you thought I’d let our Counter
component off the hook—never!), what if we, for some unfathomable reason, wanted to attach similar click handlers to the document in a second component? Maybe one that generates a random number on click.
// RandomNumberGenerator.jsx
import React, { useState, useEffect, useCallback } from "react";
const RandomNumberGenerator = () => {
const [randomNumber, setRandomNumber] = useState();
const getRandomNumber = useCallback(
() => setRandomNumber(4), // guaranteed to be random
[setRandomNumber],
);
useEffect(() => {
document.addEventListener("click", getRandomNumber);
return () => {
document.removeEventListener("click", getRandomNumber);
};
}, [getRandomNumber]);
return (
<div>
Random number is: {randomNumber}
</div>
);
};
Well, we could just redefine the logic inside our new component, but that’s no fun. If we were feeling clever, we might use a higher order component, or a render prop to do this. But this is where custom hooks can really shine. We can abstract the shared logic out to a custom hook, which we might call useDocumentClick
.
// useDocumentClick.js
import { useEffect } from "react";
function useDocumentClick(onDocumentClick) {
useEffect(() => {
document.addEventListener("click", onDocumentClick);
return () => {
document.removeEventListener("click", onDocumentClick);
};
}, [onDocumentClick]);
}
export default useDocumentClick;
Custom hooks can be used just like you would any other hook. The name of your custom hook should always start with use
, but beyond that, you can feel free to do whatever you'd like in there—including using other hooks like useState
and useEffect
.
We can then use the new hook in our components like so:
// Counter.jsx
import React, { useState, useCallback } from "react";
import useDocumentClick from "./useDocumentClick";
const Counter = () => {
const [currentCount, setCurrentCount] = useState(0);
const incrementCounter = useCallback(
() => setCurrentCount(currentCount + 1),
[setCurrentCount, currentCount],
);
useDocumentClick(incrementCounter);
return (
<div>
Current count: {currentCount}
</div>
);
};
// RandomNumberGenerator.jsx
import React, { useState, useCallback } from "react";
import useDocumentClick from "./useDocumentClick";
const RandomNumberGenerator = () => {
const [randomNumber, setRandomNumber] = useState();
const getRandomNumber = useCallback(
() => setRandomNumber(4), // guaranteed to be random
[setRandomNumber],
);
useDocumentClick(getRandomNumber);
return (
<div>
Random number is: {randomNumber}
</div>
);
};
Your component never needs to know the implementation details of the hook—just its API. This can help move complex state or side effect logic out of your components, and makes that logic easy reusable down the road. You won’t necessarily want to do this for all of your hook usage, but it’s a much easier approach to making component logic reusable when you need it to be, and can have the added benefit of making your component more easily readable.
For example, if you find yourself making API calls from your components pretty often, you might write a hook called useAPI
:
function useAPI(method, endpoint, data) {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const [response, setResponse] = useState(null);
useEffect(async () => {
try {
setIsLoading(true);
setResponse(null);
setError(null);
const res = await fetch(endpoint, { method, data });
setIsLoading(false);
setResponse(res);
} catch(err) {
setIsLoading(false);
setError(err);
}
}, [method, endpoint, data]);
return {
response,
error,
isLoading,
};
}
This way you have one consistent layer for talking with your API from a component. If you’re using modern technologies like GraphQL and Apollo, like we’re starting to at OkCupid, there’s already some great open source projects to provide you with a few of these powerful custom hooks, as well as growing collections of other assorted utility hooks.
A word of warning ⚠️
There’s one major rule you have to remember when using hooks: your hooks must always be used in the same order, every time the component renders. What that means is: hooks cannot be called inside conditionals, after early returns, or in loops. Hooks must always be called at the “top level” of indentation. If this seems weird, it’s because by Javascript standards, it’s a pretty unusual limitation. It’s an additional constraint on top of the language, but a necessary one in order for React to be able to preserve state in the right way. For more information on why this limitation exists, I’d recommend you read Dan Abramov’s blog post about this on Overreacted. The good news is: there’s an eslint
plugin to help you guard against making that mistake.
Are you hooked yet?
React hooks can really change the way we think about state and state updates within our components, which can lead to some really great refactoring opportunities. While it can be tempting to “translate” our components 1:1 from classes to hooks, this can often limit the benefits hooks can provide. I hope this guide has helped pique your interest in the power of hooks, as well as helping reframe how we think about some common patterns in our components. The opportunities for optimizing our existing code using useCallback
and useMemo
cannot be overstated, as they can provide some easy performance wins in our existing functional components. The debate about when it’s more intuitive to use hooks versus using class components will surely rage on, but I think at the very least, hooks provide us with some very powerful new tools in our tool belts for expressing stateful components and even optimizing stateless ones.