Writing Your First React Hooks

Note: The first version of this post was published on Codesmith's official blog.

React hooks were first introduced to the world in February 2019 with the release of React 16.8, and they’ve quickly become one of the library’s hottest features. However, there’s a lot to know about hooks (the official docs list ten different ones!).

Let’s start with a quick introduction to what hooks are. Simply put, they’re functions that let you “hook into” different features of React.

While hooks are completely opt-in (React works fine without them), they can open up a whole new world of possibilities. In this article, we’re going to look at two hooks — and we’ll even build a couple of apps with them!

First we’ll examine useState, a hook that’s extremely handy for managing state inside your apps. It’s a great first hook to learn. We’ll also look at useReducer, an alternative to useState that will look very familiar if you’ve used Redux.

(I’ll include links to a few different apps throughout this post — while I’ll also include plenty of code snippets to illustrate each point, feel free to open each app and play around to further your own understanding!)

Why useState?

There are lots of useful hooks to choose from — for example, useEffect is one of the most commonly used, because it provides a modular alternative to React’s lifecycle methods.

However, useState is the first hook that most React developers learn, because it’s quick to pick up and implement. It’s a great jumping-off point into other hooks, including one we’ll discuss a little later. Once you’re feeling confident with hooks, you can even build your own!

Let's Build a Counter

Let’s forget about hooks for a second, and look at how we handled state prior to React 16.8. To do this, we’ll look at a simple counter app written without any hooks.

Here’s a quick rundown of how we’re handling state in this app. First, we have a single piece of state, called count. We can also decrement, increment, and reset the count. Below are the steps we need to take to implement this in our code...

  1. Declare the state using a constructor object inside our component.
class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }
// ...
}
  1. Define each of our three methods.
  increment() {
    this.setState({ count: this.state.count + 1 });
  }
  decrement() {
    this.setState({ count: 0 });
  }
  reset() {
    this.setState({ count: this.state.count + 1 });
  }
  1. Go back to our constructor and bind each method to this.
class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
    this.increment = this.decrement.bind(this);
    this.decrement = this.increment.bind(this);
    this.reset = this.reset.bind(this);
  }
  increment() {
    this.setState({ count: this.state.count + 1 });
  }
  decrement() {
    this.setState({ count: this.state.count - 1 });
  }
  reset() {
    this.setState({ count: 0 });
  }
// ...
}

That’s 17 lines of code! Can we shorten this using hooks? Let’s give it a shot.

const App = () => {
  const [count, setCount] = useState(0);

  const increment = () => {
    setCount(prevCount => prevCount - 1);
  };
  const decrement = () => {
    setCount(0);
  };
  const reset = () => {
    setCount(prevCount => prevCount + 1);
  };
// ...
}

Using the useState hook, we managed to condense our state management logic into just 10 lines of code. (Click here to see how it looks in the context of the whole app!)

We’ll go over what’s actually happening later in this article, but note how much cleaner our code already looks. We declare our state on line 5 (instead of packing it inside a constructor function), and we define our methods directly below it. Another thing to note is that we didn’t need to bind our methods to this, saving us a lot of space in our code.

One reason that hooks are so powerful (and popular) is that they let us modularize the logic inside our components. In just a few lines, we declared our state and our methods. All without dealing with unnecessary constructor functions or this binding. Now that we’ve seen some of the benefits of hooks, let’s dive a little deeper.

Getting Ready for Hooks

There are a few things you need to know before implementing hooks in your React apps.

First, a quick note about class components and function components (aka “functional components”). If you’ve never worked with hooks before, you’ve probably written a lot of class components like the one below (let’s omit the constructor for now).

import React from 'react';

class App extends Component {
  render() {
    return (
      <div className="app">
        <h1>Hello world!</h1>
      </div>
    )
  }
}

When using hooks, you need to use function components. Functional components aren’t new to React — in the past they were known as “stateless” components because they could not declare a state object of their own.

However, hooks like useState and useReducer give you the ability to initialize state inside a functional component. We’ll write our component as an arrow function, although you can also write functional components with the function keyword (just as you can declare functions both ways in modern JavaScript).

import React, { useState } from 'react';

const App = () => {
  return (
    <div className="app">
      <h1>Hello world!</h1>
    </div>
  )
}

Note that we’ve imported the useState hook from React on the first line. Whenever you want to use a hook in a component, you need to import it the same way.

However, there’s one big problem with this component. We aren’t actually using useState. Let’s change that.

How to Implement useState

Here’s what the useState hook looks like in its simplest form.

const [count, setCount] = useState(0);

Inside the square brackets, we’re creating a piece of state called count. We’re then creating a function called setCount that sets the value of our count state.

On the right side, we’re setting the value of count to 0.

You can name the items inside the square brackets whatever you want. The convention is to use the pattern shown above, where the name of the second value is the same as the first, but with “set” in front of it. These values — the state label and the function that modifies it — are sometimes called the getter and setter.

If you want to dig deeper into what’s actually happening here, check out MDN’s section on array destructuring, or read the introduction to the useState hook in the React docs.

What if we wanted to add another piece of state? A second counter, for instance? Easy — just stack them.

const [count, setCount] = useState(0);
const [otherCount, setOtherCount] = useState(5);

The setter functions (setCount and setSecondCount) work similarly to this.setState, but they're a little cleaner.

For instance, if we wanted to reset the count to 0, we could make a tiny reset function.

const reset = () => {
  setCount(0);
}

OK, and what if we wanted to create an increment function? (No peeking at the example from earlier!) It might seem logical to reference the count state and just increment it, right?

const increment = () => {
  setCount(count + 1);
}

While this pattern may feel intuitive, it can create bugs. For instance, if you run five increment functions at once, count won't increment from 0 to 5 — it'll only move to 1!

console.log(count); // 0

const incrementFiveTimes = () => {
  increment();
  increment();
  increment();
  increment();
  increment();
}

incrementFiveTimes();

console.log(count); // 1

This is because each increment function is interacting with the same state value. You're actually just changing 0 to 1 five different times.

Instead, you can write an anonymous function inside the parentheses after setCount.

const increment = () => {
  setCount(prevCount => prevCount + 1);
}

The function's parameter gives you access to the current state when setCount runs. Therefore, the first time your updated increment function runs, prevCount1 will be equal to 0, and it will increment 0 to 1. The second time it runs, prevCount will be equal to the current count of 1, and the function will increment it to 2. Run it five times in all, and count will be equal to 5.

console.log(count); // 0

const incrementFiveTimes = () => {
  increment();
  increment();
  increment();
  increment();
  increment();
}

incrementFiveTimes();

console.log(count); // 5

Great — now we’ve learned how to declare and set state using the useState hook!

useReducer - An Alternative to useState

useState is a powerful part of your React toolbox, but it’s far from the only hook that manages state. If your app’s state is more complex — for instance, if you have a form component with inputs for a user’s name, email, password, phone number, and so on — you might choose useReducer.

As its name implies, useReducer lets you set up a reducer function to handle state changes. You're probably familiar with reducers if you've worked with Redux. However, unlike Redux's reducers, useReducer is meant for handling state inside a single component — not for your entire app.

Let’s use a simple to-do list as an example.

Here’s an example of the useReducer logic for the list. (View the whole app here)

const ADD_TASK = "ADD_TASK";
const REMOVE_TASK = "REMOVE_TASK";
const CHECK_TASK = "CHECK_TASK"

const reducer = (state, action) => {
  switch (action.type) {
    case ADD_TASK:
      return [...state, action.payload];
    case REMOVE_TASK:
      const { id } = action.payload;
      return state.filter(task => task.id !== id);
    default:
      return state;
  }
};

What if we refactored this app to implement useState? (Check out the refactored app here)

const [tasks, setTasks] = useState(initialState);
  const addNewTask = task => {
    setTasks(prevState => [...prevState, task]);
  };
  const removeTask = id => {
    setTasks(prevState => {
      return prevState.filter(task => task.id !== id);
    });
  };

While the useState implementation is shorter, the useReducer version is much more scalable. Currently, we can add and remove tasks. But what if we wanted to introduce other features, like being able to “check off” a task before permanently deleting it? In that case, we could very easily create a CHECK_TASK action and integrate the logic into our reducer function.

const reducer = (state, action) => {
  switch (action.type) {
    case ADD_TASK:
      return [...state, action.payload];
    case REMOVE_TASK:
      const { id } = action.payload;
      return state.filter(task => task.id !== id);
    case CHECK_TASK:
      return state.map(task => {
        if(task.id !== id) return task;
        return {...task, checked: !task.checked}
      })
    default:
      return state;
  }
};

Wrapping Up

In this post we learned what hooks are — functions that “hook into” React features. We also learned about useState and useReducer — two powerful hooks for managing state in your React apps.

It’s interesting to note that neither of these hooks actually change how state works in your apps. If you wanted, you could strip away the hooks in our to-do list and refactor the app to use traditional class component state. But now that you know how hooks like useState and useReduce make state management so much more intuitive... would you really want to?

Apps used in this post

References