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!)
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.
2) Define each of our three methods.
3) Go back to our constructor and bind each method to this.
That’s 17 lines of code! Can we shorten this using hooks? Let’s give it a shot.
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 (also known as “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).
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.
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
In its simplest form, here’s what the useState hook looks like.
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.
What if we wanted to add another piece of state? A second counter, for instance? Easy — just stack them.
The setter function works similarly to this.setState, but it’s a little cleaner.
For instance, if we wanted to reset the count to 0, we could make a tiny “reset” function.
OK, and what if we wanted to create an “increment” function? (No peeking above!) It might seem logical to reference the “count” state and just increment it, right?
While this may feel right, following this pattern can lead to bugs. For instance, if you run five increment functions at once, the count won't go from 0 to 5 — it'll increment to 1!
Instead, you can write an anonymous function inside the parentheses after setCount. The function parameter gives you access to the current state right when setCount runs.
If you ran this function five times at once, our count would increment to 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 tool for 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. The implementation of this hook will seem very familiar if you’ve used Redux before — however, unlike Redux, useReducer is meant for handling state inside a single component rather than 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)
Let’s compare that to how we would implement useState for that same list (or check out the refactored app here).
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.
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?
APP EXAMPLES IN THIS POST