Using Hooks to Tame Your Stateful Logic ↘

Louis Novick
Development

When hooks were announced at React Conf 2018, it wasn’t immediately apparent to me what they were selling.  The new API was touted as the way React components would be written in the future, and that these hooks would greatly simplify a lot of what we do in React today.  Some lifecycle methods like componentDidMount componentDidUpdate and componentDidUnmount could be replaced by a single useEffect hook, while other APIs such as context and refs would have hooks of their own, making existing patterns more declarative and making achieving similar functionality more straightforward.

In recent months, the way I’ve worked on React Apps has significantly changed as was foretold, and I want to share some of what I’ve learned, as well as strategies for working with hooks. I’ll also go over how making custom hooks can be a great way to help you tame your stateful logic.

Getting Hooked

If you haven’t started exploring hooks you’re missing out on what has been, for many, one of the greatest new additions to React.

Hooks introduce a paradigm shift for managing and reusing stateful logic in React components that is comparable, in my opinion, to what “Styled Components” was and is for styling React components:

  • A declarative API
  • An intuitive approach
  • A small learning curve with a big payoff

It’s a notable improvement with many benefits that just makes sense.  Once I started using hooks in the apps I work on, it felt like it was more work not to use them. A large class component filled with lifecycle methods started to be a code smell.

A huge benefit to using hooks is that there are a multitude of other tools/libraries available that export their own hooks. Libraries like React-use are great examples of this, exporting custom hooks like useAsyncRetry.

Peeking under the hood of useAsyncRetry we can see that it is using useState to manage the attempt count.  The author of React-use is also wrapping their own custom hooks in more custom hooks! useAsync and useCallback.

useAsyncRetry innards

const [attempt, setAttempt] = useState(0);
const state = useAsync(fn, [...deps, attempt]);
const stateLoading = state.loading;
const retry = useCallback(() => {
  ...
}
Love It GIF by Late Night with Seth Meyers - Find & Share on GIPHY

I’ll cover how to make your own custom hooks in another section. For now, let’s explore some of the differences that using hooks can introduce into your codebase.

How does it compare? 

An intentional separation of stateful components and presentational components lies at the heart of any well-organized app. Traditionally in React, you create a class containing state, and maybe a few different lifecycle calls. This class component would then pass down its state along with any other related methods to child components as props. Its children, at least those concerned with presentation, would ideally be completely stateless and have a single responsibility, create an accurate representation of your UI based on the data they receive.

A class component

import React, { Component } from "React";
import Card from "./card";

export default class Container extends Component {
  constructor(props) {
    super(props);

    this.state = {
      data: []
    };
  }

  componentDidMount() {
  }

  componentDidUpdate() {
  }

  componentWillUnmount() {
  }

  render() {
    return this.state.data.map(item => {
      return (
        <Card
          key={item.id}
          title={item.title}
          description={item.description}
        />
      );
    });
  }
}

A function component

import React from "React";

const Card = ({ title, description }) => {
  return (
    <div>
      <h3>{title}</h3>

      <p>{description}</p>
    </div>
  );
};

export default Card;

Hooks give us the ability to place stateful logic within function components. Some of your function components will get a bit larger, but really you’re just replacing your class components with leaner version of themselves.  You should keep separating stateful components and presentational components.

Here is the same class component above as a function component using hooks.

import React, { useEffect, useState } from "React";
import Card from "./card";

const Container = () => {
  const [data, setData] = useState([]);

  useEffect(() => {
    // Set state with some data
    setData([]);

    return () => {
      // When this component dismounts
    };
  }, []); // Empty array will only fire on mount. 

  return data.map(item => {
    return (
      <Card
        key={item.id}
        title={item.title}
        description={item.description}
      />
    );
  });
};

export default Container;`

You’re left with the same stateful component functionality-wise, but with arguably less noise. And the true beauty of it is? The ‘how’ and ‘why’ of reusing recurrent stateful code is now much clearer.  I could start to get clever here and decide to split out this component’s useState and useEffect into a custom hook called useData. I can then import and use useData in any component with similar needs.  Now let’s explore what goes into making a custom hook.

Making a custom hook

Striker Forging GIF by Ken's Custom Iron - Find & Share on GIPHY

For simplicity’s sake, we’ll create a hook that manages a window resize event. It will manage the binding and unbinding of the event, as well as return the desired values to a callback function that fires on every resize.

We’ll start by setting up a hooks folder in our project, and creating a file we’ll call useResize.  

import { useEffect } from "React";

const useResize = callback => {
  useEffect(() => {
    const handleResize = () => {
      callback({
        height: window.innerHeight,
        width: window.innerWidth
      });
    };

    window.addEventListener("resize", handleResize);

    return () => {
      window.removeEventListener("resize", handleResize);
    };
  }, []);
};

export default useResize;

We’re wrapping a useEffect hook in what appears to be just another function component.  This useEffect is adding a resize event listener on mount, and removes that same listener on dismount.

Function component that needs to know the window size

import React from "React";
import useResize from "../hooks/useResize";

const Container = () => {
  useResize(report => {
    console.log(report);
  });

  return <div>{/* ... */}</div>;
};

export default Container;

This component calls useResize, and is now able to utilize that data any way it needs to without having to do any of the heavy lifting. We’ve decoupled the lifecycle logic associated with managing an event like this, and can re-use it anywhere we see fit. The possibilities are endless.

Embrace the change

Line Fishing GIF - Find & Share on GIPHY

A few key things to keep in mind when using hooks is that they:

  • Can only be used in function components
  • Run in the order they are called in
  • Operate in isolation

Now that we’ve taken a look at what hooks are at a high level, and what goes into making your own custom hooks – you should try them at home. If this article hasn’t convinced you, that’s perfectly fine. The React team has already stated that there is currently no plan to remove classes from React. Hooks exist simply to provide an alternative API to the “traditional” way of writing your components, another tool in your belt. I think you’ll find that once you take the plunge, it’ll be hard to write React code any other way.