Ruben Martinez Jr. in javascript, frontend, react, hooks

Getting Hooked on React Hooks

2twdtl

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!”

82BCA13D-8A45-4F10-822B-34D567B52973
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:

tumblr_o16n2kBlpX1ta3qyvo1_1280
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, don’t forget to .bind every function that’s going to be modifying state in the constructor, or remember to use the arrow function syntax someone on your team installed a babel plugin for a few years ago.

tumblr_inline_p2dyybPLrV1qgoj6i_540

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:

  1. We need to know the current click count (let’s call this currentCount)
  2. 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:


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.


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 a value and a setter for that state value 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:


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:


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:

  1. componentDidMount and componentWillUnmount for attaching/removing event listeners, or setting/clearing a timeout. (Example: listening to document scroll or keypress events to change state)
  2. componentDidMount and componentDidUpdate for loading something based on a prop/state change. (Example: loading data when we land on a page, and reloading when state changes)
  3. componentDidMount and componentDidUpdate 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.


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 rendered three times, it would run the functions as follows:

  1. render
  2. setUp()
  3. render (x2)
  4. tearDown()
  5. setUp()
  6. render (x3)
  7. tearDown()
  8. 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!


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!


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:


import React from "react";

const MyComponent = ({ 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:


import React, { useMemo } from "react";

const MyComponent = ({ 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 click handlers to the document in a second component? Maybe one that generates a random number on click.


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.


import React, { useState, useEffect, useCallback } from "react";

function useDocumentClick(onDocumentClick) {
    useEffect(() => {
        document.addEventListener("click", onDocumentClick);

        return () => {
            document.removeEventListener("click", onDocumentClick);
        };
    }, [onDocumentClick]);
}

const Counter = () => {
    const [currentCount, setCurrentCount] = useState(0);
    const incrementCounter = useCallback(
        () => setCurrentCount(currentCount + 1),
        [setCurrentCount, currentCount],
    );
    useDocumentClick(incrementCounter);
	
    return (
        <div>
            Current count: {currentCount}
        </div>
    );
};

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>
    );
};

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. Your component never needs to know the implementation details of a 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 hooks, but it’s a much easier approach to making component logic reusable when you need it to be.

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 be declared in the same order, every time the component renders. What that means is: hooks cannot be defined inside conditionals, after conditional 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 an 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 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.