React Hooks: How to Get Started & Build Your Own

React Hooks are special functions that allow you to “hook into” React features. For example, the useState hook allows you to add React state to a functional component. useEffect is another hook that allows you to perform side effects in function components. Side effects are usually implemented using lifecycle methods. With hooks, this is no longer necessary.

This means you no longer need to define a class when constructing a React component. It turns out that the class architecture used in React is the cause of a lot of challenges that React developers face every day. We often find ourselves writing large complex components that are difficult to break up. Related code is spread over several lifecycle methods, which becomes tricky to read, maintain and test. In addition, we have to deal with the this keyword when accessing state, props and functions. We also have to bind functions to this to ensure they are accessible within the component. Then we have the excessive prop drilling problem — also known as wrapper hell — when dealing with higher order components.

In a nutshell, hooks is a revolutionary feature that will effectively simplify your code, making it easy to read, maintain, test in isolation and re-use in your projects. It will only take you an hour to learn. Soon, you will start thinking very differently about the way you write React code.

React Hooks was first announced at a React conference that was held in October 2018. It was officially made available in React 16.8 last month. This feature is still under development — there are still a number of React class features being migrated into hooks. The good news is that you can start using them now. You can still use React class components if you want to — however, I doubt you will after you finish this introductory guide.

If I’ve grabbed your curiosity, let’s dive in and see some practical examples.

Prerequisites

This article is for intermediate to advanced React developers. If you are a beginner, please go through the following tutorials first:

I won’t cover how to create a new React project or other beginner-level skills. You should be able to follow this guide easily. You can access the completed project on GitHub.

useState Hook

Consider the following React class component:

import React from "react";

export default class ClassDemo extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      name: "Agata"
    };
    this.handleNameChange = this.handleNameChange.bind(this);
  }

  handleNameChange(e) {
    this.setState({
      name: e.target.value
    });
  }

  render() {
    return (
      <section>
        <form autocomplete="off">
          <section>
            <label htmlFor="name">Name</label>
            <input
              type="text"
              name="name"
              id="name"
              value={this.state.name}
              onChange={this.handleNameChange}
            />
          </section>
        </form>
        <p>Hello {this.state.name}</p>
      </section>
    );
  }
}

This is how it looks:

React Hooks Class Name

When you update the name field, the ‘Hello *’ message should update as well. Give yourself a minute to understand the code. Next, we are going to write a new version of this code using a React Hook known as useState.

Its syntax looks like this:

const [state, setState] = useState(initialState);

When you call the useState function, it returns two items:

  • state – the name of your state, e.g. this.state.name or this.state.location
  • setState – a function for setting a new value for your state. Similar to this.setState({name:newValue})

The initialState is the default value you give to your newly declared state during the state declaration phase. Now that you have an idea of what useState is, let’s put it in action.

import React, { useState } from "react";

export default function HookDemo(props) {
  const [name, setName] = useState("Agata");

  function handleNameChange(e) {
    setName(e.target.value);
  }

  return (
    <section>
      <form autocomplete="off">
        <section>
          <label htmlFor="name">Name</label>
          <input
            type="text"
            name="name"
            id="name"
            value={name}
            onChange={handleNameChange}
          />
        </section>
      </form>
      <p>Hello {name}</p>
    </section>
  );
}

Take note of the differences between this version and the class version. It’s already much more compact and easier to understand than the class version, yet they both do exactly the same thing. Let’s go over the differences:

  • The entire class constructor has been replaced by the useState Hook, which only consists of a single line.
  • Because the useState Hook outputs local variables, you no longer need to use the this keyword to reference your function or state variables. Honestly, this is a major pain for most JavaScript developers, as it’s not always clear when you should use this.
  • JSX code is now cleaner as you can reference local state values without using this.state.

I hope you are impressed by now! You may be wondering what to do when you need declare multiple state values. The answer is quite simple: just call another useState hook. You can declare as many times as you want, provided you are not overcomplicating your component.

Make sure to do it at at the top, and never inside a condition. Here is an example of a component with multiple useState Hooks:

import React, { useState } from "react";

export default function HookDemo(props) {
  const [name, setName] = useState("Agata");
  const [location, setLocation] = useState("Nairobi");

  function handleNameChange(e) {
    setName(e.target.value);
  }

  function handleLocationChange(e) {
    setLocation(e.target.value);
  }

  return (
    <section>
      <form autocomplete="off">
        <section>
          <label htmlFor="name">Name</label>
          <input
            type="text"
            name="name"
            id="name"
            value={name}
            onChange={handleNameChange}
          />
        </section>
        <section>
          <label htmlFor="location">Location</label>
          <input
            type="text"
            name="location"
            id="location"
            value={location}
            onChange={handleLocationChange}
          />
        </section>
      </form>
      <p>
        Hello {name} from {location}
      </p>
    </section>
  );
}

Quite simple, isn’t it? Doing the same thing in the Class version would require you to use even more this keywords. Let’s move on to the next basic React Hook.

useEffect Hook

Most React components are required to perform a specific operation such as fetching data, subscribing, or manually changing the DOM. These kind of operations are known as side effects.

We usually put our side effects code into componentDidMount and componentDidUpdate. These are lifecycle methods that allows us to trigger the render method at the right time.

Here is a simple example:

componentDidMount() {
  document.title = this.state.name + " from " + this.state.location;
}

This piece of code will simply set the document title. However, when you try making changes to the state values via the form, nothing happens. To fix this, you need to add another lifecycle method:

componentDidUpdate() {
    document.title = this.state.name + " from " + this.state.location;
  }

Updating the form should now update the document title as well.

React Hooks Class Title

Let’s see how we can implement the same logic using the useEffect Hook:

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

useEffect(() => {
  document.title = name + " from " + location;
});

With just those few lines of code, we have implemented the work of two lifecycle methods in one simple function.

This was a simple example. However, there are cases where you need to write clean-up code, such as unsubscribing from a data stream or unregistering from an event listener. Let’s see an example of how this is normally implemented in a React class component:

//...
  constructor(props) {
    super(props);
    this.state = {
      name: "Agata",
      location: "Nairobi",
      resolution: {
        width: window.innerWidth,
        height: window.innerHeight
      }
    };
    //...
    this.handleResize = this.handleResize.bind(this);
  }

  componentDidMount() {
    document.title = this.state.name + " from " + this.state.location;
    window.addEventListener("resize", this.handleResize);
  }

  componentDidUpdate() {
    document.title = this.state.name + " from " + this.state.location;
    window.addEventListener("resize", this.handleResize);
  }
  //...
   handleResize() {
    this.setState({
      resolution: {
        width: window.innerWidth,
        height: window.innerHeight
      }
    });
  }

  render() {
    return (
      <section>
        ...
        <h3>
          {this.state.resolution.width} x {this.state.resolution.height}
        </h3>
      </section>
    )
  }

Adding the above code will display the current resolution of your browser window. Resize the window and you should see the numbers update automatically. If you press F11 in Chrome, it should display the full resolution of your monitor.

React Hooks Class Resolution

Let’s replicate the above “class” code to our “hook” version. We’ll need to define a third useState and a second useEffect function to handle this new feature:

//... state declaration
const [resolution, setResolution] = useState({
  width: window.innerWidth,
  height: window.innerHeight
});
//...
useEffect(() => {
  const handleResize = () => {
    setResolution({
      width: window.innerWidth,
      height: window.innerHeight
    });
  };
  window.addEventListener("resize", handleResize);
  return () => {
    window.removeEventListener("resize ", handleResize);
  };
});
//... jsx render
<h3>
  {resolution.width} x {resolution.height}
</h3>;

Amazingly, this hook version of the code does the same exact thing. It’s cleaner and more compact. The advantage of putting code into its own useEffect declaration is that we can easily test it, since the code is in isolation.

Have you noticed that we are returning a function in this useEffect hook? This is because any function you return inside a useEffect function will be considered to be the code for clean-up. If you don’t return a function, no clean-up will be done. In this case, clean-up will be required, otherwise you will encounter a bug when running the code.

Custom React Hooks

Now that you have learned about the useState and useEffect Hooks, let me show you a really cool way of making your code even more compact, cleaner and reusable than what we’ve achieved so far. We are going to create a custom hook to simplify our code even further.

We’ll do this by extracting the resize effect hook and placing it outside our component. Simply create a new function as follows:

function useWindowResolution() {
  const [resolution, setResolution] = useState({
    width: window.innerWidth,
    height: window.innerHeight
  });
  useEffect(() => {
    const handleResize = () => {
      setResolution({
        width: window.innerWidth,
        height: window.innerHeight
      });
    };
    window.addEventListener("resize", handleResize);
    return () => {
      window.removeEventListener("resize ", handleResize);
      console.log("cleanup");
    };
  });
  return resolution;
}

Next you’ll need to replace this code:

const [resolution, setResolution] = useState({
  width: window.innerWidth,
  height: window.innerHeight
});

… with this:

const resolution = useWindowResolution();

Delete the second useEffect code. Save your file and test it. Resizing the browser window should effectively update the resolution figures. In case you experience some kind of lag, then replace the useWindowResolution function with this one:

function useWindowResolution() {
  const [width, setWidth] = useState(window.innerWidth);
  const [height, setHeight] = useState(window.innerHeight);
  useEffect(() => {
    const handleResize = () => {
      setWidth(window.innerWidth);
      setHeight(window.innerHeight);
    };
    window.addEventListener("resize", handleResize);
    return () => {
      window.removeEventListener("resize ", handleResize);
    };
  }, [width, height]);
  return {
    width,
    height
  };
}

It seems there might be a bug that is causing a delay in the clean-up code. Using primitives for local state fixes that problem. By the time you are reading this, you may not be experiencing this issue.

Now that we’ve created our first custom hook, let’s do the same for the document title. You’ll need to identify the relevant code that needs to be replaced on your own. I’ll just show you the code for the custom hook:

//...
const resolution = useWindowResolution();
useDocumentTitle(name + " from " + location);
//...

function useDocumentTitle(title) {
  useEffect(() => {
    document.title = title;
  });
}

The document title should change just like before. Now let’s refactor the form fields. The refactored component will look like this:

export default function HookDemo(props) {
  const name = useFormInput("Agata");
  const location = useFormInput("Nairobi");
  const resolution = useWindowResolution();
  useDocumentTitle(name.value + " from " + location.value);

  return (
    <section>
      <form autoComplete="off">
        <section>
          <label htmlFor="name">Name</label>
          <input {...name} />
        </section>
        <section>
          <label htmlFor="location">Location</label>
          <input {...location} />
        </section>
      </form>
      <p>
        Hello {name.value} from {location.value}
      </p>
      <h3>
        {resolution.width} x {resolution.height}
      </h3>
    </section>
  );
}

function useFormInput(initialValue) {
  const [value, setValue] = useState(initialValue);

  function handleChange(e) {
    setValue(e.target.value);
  }

  return {
    value,
    onChange: handleChange
  };
}

Go through the code slowly and identify all the changes we have made. Pretty neat, right? Our component is much more compact. We can package useFormInput, useDocumentTitle and useWindowResolution into an external npm module since they are completely independent from the main logic. We can easily reuse these custom hooks in other parts of the project, or even other projects in the future.

For reference, here is the complete hooks component version:

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

export default function HookDemo(props) {
  const name = useFormInput("Agata");
  const location = useFormInput("Nairobi");
  const resolution = useWindowResolution();
  useDocumentTitle(name.value + " from " + location.value);

  return (
    <section>
      <form autoComplete="off">
        <section>
          <label htmlFor="name">Name</label>
          <input {...name} />
        </section>
        <section>
          <label htmlFor="location">Location</label>
          <input {...location} />
        </section>
      </form>
      <p>
        Hello {name.value} from {location.value}
      </p>
      <h3>
        {resolution.width} x {resolution.height}
      </h3>
    </section>
  );
}

function useDocumentTitle(title) {
  useEffect(() => {
    document.title = title;
  });
}

function useFormInput(initialValue) {
  const [value, setValue] = useState(initialValue);

  function handleChange(e) {
    setValue(e.target.value);
  }

  return {
    value,
    onChange: handleChange
  };
}

function useWindowResolution() {
  const [width, setWidth] = useState(window.innerWidth);
  const [height, setHeight] = useState(window.innerHeight);
  useEffect(() => {
    const handleResize = () => {
      setWidth(window.innerWidth);
      setHeight(window.innerHeight);
    };
    window.addEventListener("resize", handleResize);
    return () => {
      window.removeEventListener("resize ", handleResize, true);
    };
  }, [width, height]);
  return {
    width,
    height
  };
}

The hook’s component should render and behave exactly like the class component version:

React Hooks Final

If you compare it with the class component version, you will realize that the hook feature reduces your component code by at least 30%. You can even reduce your code further by exporting the reusable functions to a npm library.

This is the end of our introductory guide, but there’s so much about hooks I haven’t covered. I’ll give you some pointers for your own exploration in the next section.

Official React Hooks

These are the basic React Hooks that you will definitely use in every React project:

  • useState – for managing local state
  • useEffects – replaces lifecycle functions
  • useContext – allows you to easily work with the React Context API (solves the prop drilling issue)

We also have additional official React Hooks which you may use depending on your project requirements:

  • useReducer – An advanced version of useState for managing complex state logic. It’s quite similar to Redux
  • useCallback – Returns a function that returns a cacheable value. Useful for performance optimization if you want to prevent unnecessary re-renders when the input hasn’t changed.
  • useMemo – Returns a value from a memoized function. Similar to computed if you are familiar with Vue.
  • useRef – Returns a mutable ref object that persists for the full lifetime of the component.
  • useImperativeHandle – customizes the instance value that is exposed to parent components when using ref.
  • useLayoutEffect – Similar to useEffect, but fires synchronously after all DOM mutations.
  • useDebugValue – Displays a label for custom hooks in React DevTools.

Some of these additional hooks are a little bit advanced, and will require separate tutorials.

Summary

The React community has responded positively to the new React Hooks feature. There’s already an open-source repository called the awesome-react-hooks. Hundreds of custom React Hooks have been submitted to this repository. Here is a quick example of one of those hooks for storing values in local storage:

import useLocalStorage from "@rehooks/local-storage";

function MyComponent() {
  let name = useLocalStorage("name"); // send the key to be tracked.
  return (
    <div>
      <h1>{name}</h1>
    </div>
  );
}

You will need to install the local-storage hook with npm or yarn like this to use it:

yarn add @rehooks/local-storage

Pretty neat, right? The introduction of React Hooks has made a big splash. Its waves have moved beyond the React community into the JavaScript world. This is because hooks is a new concept that can benefit the entire JavaScript ecosystem. In fact, the Vue.js team has already begun implementing their own version.

There’s also talk of React Hooks and the Context API overthrowing Redux from its state management throne. Clearly, hooks has made coding much simpler and has changed the way we will write new code. If you are like me, you probably have a strong urge to rewrite all your React component classes and replace them with functional component hooks.

Do note this not really necessary — the React team doesn’t plan to deprecate React class components. You should also be aware that not all React class lifecycle methods are possible with hooks yet. You may have to stick with React component classes a bit longer.

If you feel confident enough with your new knowledge of basic React Hooks, I would like to challenge you with a task. Refactor this Countdown timer class using React hooks to make it as clean and compact as possible. Happy coding!

Sponsors