What We Will Learn
Welcome to our exhaustive React Hooks guide. Follow along and fork my code at any point as we learn about everything from the basics all the way to advanced hooks usage examples such as useReducer
for managing a list of data. This guide also provides background on the history of React and its concept of state management. Join me as we explore the ins and outs of React Hooks!
This article was last updated in October of 2021.
Chapter 1
What You Should Know About React Hooks
First released in October of 2018, the React hook APIs provide an alternative to writing class-based components, and offer an alternative approach to state management and lifecycle methods. Hooks bring to functional components the things we once were only able to do with classes, like being able to work with React local state, effects and context through useState
, useEffect
and useContext
.
Additional Hooks include: useReducer
, useCallback
, useMemo
, useRef
, useImperativeHandle
, useLayoutEffect
and useDebugValue
. You can read about these APIs in the React Hooks API Reference!
So How Do We Use Hooks
The easiest way to describe Hooks is to show side-by-side examples of a class component that needs to have access to state and lifecycle methods, and another example where we achieve the same thing with a functional component.
Below I provide a working example similar to those in the ReactJS docs, but one that you can touch and play around with, getting your hands dirty with a StackBlitz demo for each stage of our learning. So let's stop talking and start learning about React Hooks.
The Benefits of Using Hooks
Hooks have a lot of benefit to us as developers, and they are going to change the way we write components for the better. They already help us to write clearer and more concise code - it's like we went on a code diet and we lost a lot of weight and we look better and feel better. It brings out our jawline and makes us feel lighter on our toes. It's the one change that works for us. Just look at what React Hooks have done for others!
All kidding aside, Hooks do trim the fat. It cuts down and makes our code more readable, concise and clear. To demonstrate, let's check out a class version of our canonical “document title effect” and see the difference between how we used to write something like this side by side with an example using an npm-installed Hook that does the same thing.
The side-by-side below shows how the component has lost a little weight. We not only save about five lines of code, but the readability and test-ability also improve with the change over to Hooks. Switching existing code over to Hooks could have a big impact on the sheer volume of code and readability, but I would encourage you to take it slow. Remember that Hooks is backward compatible with the code it's replacing and can live side by side with it, so there's no need to rewrite the whole codebase immediately. Check out the StackBlitz demos for this code: Before and After
Five Important Rules for Hooks
Before we create our own Hook, let's review a few of the major rules we must always follow.
- Never call Hooks from inside a loop, condition or nested function
- Hooks should sit at the top-level of your component
- Only call Hooks from React functional components
- Never call a Hook from a regular function
- Hooks can call other Hooks
If you would like, you can enforce these rules in your team with an ES Lint plugin. Also, on that same page there are good explanations on why these rules are required.
Chapter 2
Hooks for State and Effects
This GIF below shows the difference between the Class and Functional Components we will see in the next section:
Class vs Functional Counter Component Example with useState
Below we follow the canonical counter component example provided in the React documentation. It's a pretty simple demo that contains a button inside of a Counter
component. Once clicked, it advances the state by one and updates the state.count
for rendering purposes.
First, we see how to do this with a class-based component using setState
:
The first thing to notice about the class-based component is that the class syntax uses a constructor, that references the this
keyword. Inside the constructor, we have a state
property. We also update the count using setState()
.
Now, let's see how to do the same thing with Hooks, using a functional component:
In the functional component example, we have an additional import of useState
. There's no more class syntax or constructor, just a const
. Its assignment sets the default and provides not only the count
property, but a function for modifying that state called setCount
. This setCount
refers to a function and can be named whatever you like.
The functional component incrementCount
method is easier to read, and references our state value directly instead of referencing this.state
.
Side-by-Side Example of useEffect
When updating state, we sometimes have side effects that need to happen along with each change. In our example of the counter, we may need to update the database, make a change to local storage or simply update the document title. In the React JS docs, the latter example is used to make things as simple as possible. So let's do just that and update our examples to have a side effect that uses the new Hook useEffect
.
Let's add this side effect to our existing counter example and again look at the old way of doing this with Classes vs working with Hooks. We will first see how to do this with a basic class-based component:
And next, how to do the same thing with Hooks:
Now that we are introducing additional behavior, we start to see even more evidence of how switching to Hooks provides a cleaner way of dealing with state and side effects. What took two separate lifecycle methods in a class component can now be achieved with one useEffect
method. Just getting rid of the class syntax and an additional method already makes our code more readable. It's totally worth it.
Call useState and useEffect as Much as You Want
Just like with setState
, you can call useState
as many times as you want. Let's switch to an example that shows a slightly more involved situation where we have a name we are displaying on the page, some inputs that allow for the name to be changed, and we want control over both the first name and the last name. We can create two separate properties, each with their own update or set function, and simply call useState
on each to set the default.
In the GIF below, you can see what this would look like - as well as what it looks like with a class-based version, which we'll dig into a little more below.
As you would expect we also have an update function for each name so that you can handle changes to them independently.
Let's see the code for the class-based component:
And now, the same thing with Hooks:
I won't go over all the differences again, but I wanted you to see a slightly more complex example side by side. Hopefully, you are starting to see the benefits of using Hooks.
Let's make one more change to this example and use useEffect
to save our name to local storage so that we don't lose our state when we refresh the page.
Let's see the code for the class-based component:
And again with Hooks:
Chapter 3
Hooks For Context
To understand the third basic React Hook useContext
, we first need to have a good understanding of the Context API, a stable feature since React 16.3. Like learning most things, sometimes we have to fully understand another concept before moving forward. If you're familiar with Context API, you can skip to the useContext
section. However, if the Context API is new to you, we'll go over it briefly and show some demos before we move on to using Context with Hooks. You cannot use useContext
without first having a Context being provided in your application. I'd also like to show you a StackBlitz demo that will serve as a starting point for the code we will work with. It picks up where we left off with exploring state and effects.
A good example of using Context is a profile component. Think about the information that might be displayed on a profile. When I'm logged into xyz.com
, I have a cloud of data that needs to be available to all or some of a profile component's children. We can assume this profile component needs two components as its children: <user>
and <team>
. One of these two components will display my user name and image, the other will display my team. We have people on the React, Angular and Vue teams, so we will use these framework names as our team names.
Back to the code, we can pass the data needed (as an object) into a <provider>
component through its value
attribute. By doing this, we allow any component and its child components to share this data.
Let's understand how to approach this component by simply passing props through to the children (an option in the pre-Context API era). When we used that “prop drilling” technique, we would propagate our data down through each child component. This created physical inputs into each component allowing data (state) to flow from the outside world into every single component and its child components.
Let's see a code demo of this pre-context era example.
If an application has 10 components, each of which has their own tree of components that in turn have a tree of components, would you rather manually pass props around to components that may or may not need the data? Or would you rather just consume that data from any point within a component tree? Context allows this sharing of values between components without having to explicitly pass a prop through every level of the tree. Let's try to visualize how the Context API by itself can be applied to class-based components:
Let's also see a working example of that in StackBlitz!
A Primer on Context API
We now need to understand how to access state with useState, how to replace a lifecycle method with useEffect and how to consume a provider using a hook called useContext.
In the Context API demo above, we saw a way to share data through many components using “prop drilling.” This is a valid option, but in some situations, it feels clunky. For better composition, we will use the Context API.
Going from our simple prop drilling example to a much more maintainable Context API example, we ended up with some code that, while it is a better solution to our problem, further creates clutter in our components. Let's look at this:
Each place I want to consume the context, I had to wrap <ProfileContext.Consumer>
tags around the DOM elements that I wanted to consume the provider. This creates additional clutter in our JSX. It would be nice to just create a const
at the top of the functional component with a hook in order to use the Profile Context wherever we want to throughout our JSX. We want our JSX to be as simple and as close to the HTML output that we desire because this makes it more readable to the developer. Hooks improve this situation and is such an elegant way of consuming context. And like I mentioned, it allows us to remove those tags that once previously cluttered up our JSX.
This is already much nicer to look at. This is one example of how Hooks will change the way that we write normal, everyday components.
This calls for an updated StackBlitz example. Let's see our Profile component now using Hooks. Not much to explain—if you remember, when we call useState
, we had a tuple of values that we needed to understand. One was the actual state and the other, a method to update the state. With useEffect,
you can clean up after each effect and only run the effect if a certain state has changed. With useContext,
we just point it at an existing context and that property now holds a reference to that context. It's that simple.
Not Defining a Provider
In the demo above, we are introduced to useContext
and since we are only reading the data inside of the context, we have passed the object/data directly into the createContext()
method. This is completely valid and we don't have to wrap the <Provider>
component around the profile component. This is not how I want to leave it though—I do want to create a provider because to be able to change the team
, I will need it. But I wanted to show that if you are passing in some data and not accessing any functions like we want to do, you can set it up without a provider.
We will revert back to using a provider because in some cases, we will need to have the ability to update some part of that context's state. For instance, I want to be able to change the Team that our user is affiliated with.
For this scenario, we will again create a <Provider>
instead of just passing in the default context like before.
Updating State Within a Provider
I want to revisit our Context API example and update it to be able to use setState
to update one of our properties. We should be able to update the Team value by calling a function that we will put inside our state as a key-value pair.
Let's ensure that the user can initiate a change in Team simply by pressing a button with the name of the team you would like to switch to. We want the change to happen from within the User component, but we also want the buttons to show up at the bottom of the Profile view.
With the addition of the buttons, we will need each one to handle a click and pass the proper team framework type to the function, which will take an argument for the name of team: Vue, Angular or React.
We will need a function that can handle changing the state. We will make a new property on our state named toggleTeam
, and its value will be the actual function which calls setState
. We will call this method through the context.
Now we can change the Team value as well as read from the context. This pattern does both the setup and subscribing for us.
I have provided another StackBlitz demo that enhances the original Context API example. Remember, this code below is not using Hooks, we will see next how to do that.
Next, we will look at another example. This one has a similar setup for the buttons and their handler function that is used to change teams
. In fact, there is no difference between the button syntax and the state object in this Hooks version. A big benefit of using Hooks is that we can omit the usage of <ProfileContext.Consumer>
tags wrapped around areas where we want to consume context, which created unwelcome HTML in our component. We will simply create a const
in each functional component that will hold a reference to our context:
const context = useContext(ProfileContext);
And we just call the context and its methods as we did before.
I'd like to show you one more example that takes our current profile component and refactors the “Change Team” buttons into their own component, and converts the Context API Provider to be functional—and instead of this.state
we will implement useState
instead. As noted in the last example, we also got rid of the <ProfileContext.Consumer>
tags and now we have useContext
. All of our components are now functional as well.
Chapter 4
Hooks for Reducers
In the past few sections, we have become familiar with the three basic React Hooks. Now let's take what we have learned and apply that knowledge to a more advanced demo using the useReducer
hook. Understanding the basic useState
hook can prepare us for learning about useReducer
so if you need a refresher, go back to the section: Hooks for State & Effect, for some reinforcement of the fundamentals.
Redux was one of the most popular ways of working with unidirectional data using reducers aside from setState and was encouraged by the React team for managing state. Since the 16.9 release, however; React now has useReducer which gives us a powerful way to use reducers without depending on the Redux library as a dependency simply to manage UI state.
A Quick Primer on Reducers
Let's talk about the difference between a Redux state reducer and the JavaScript method Array.prototype.reduce.
The canonical array prototype example is a sum function. When we call the reducer on an array that contains only numbers, we can return a single numeric value summing up all values in the array. The reducer can be fed an initial value to start with as an optional second argument. Let's briefly take a look at some code that demonstrates the reduce
method from JavaScript's Array.prototype
.
const votesByDistrict = [250, 515, 333, 410];
const reducer = (accumulator, currentValue) => {
return accumulator + currentValue;
};
console.log(votesByDistrict.reduce(reducer));
// expected output: 1508
// and below we simply add a value to start from:
console.log(votesByDistrict.reduce(reducer, 777));
// expected output: 2285
A sum function is the simplest example, but inside that reducer, you can do any work you want iteratively between the curly braces. Think of it like a recipe. Given the same input, these instructions always produce the same result. We are talking about pure functions. This is a powerful concept, especially when used to manage state.
Let's take a look at one more reducer example to help us understand them better. Instead of accumulating a value on each iteration, you could also do a comparison on each iteration. Just like the sum reducer, we store a value each time, but instead of adding up the values, we will store the highest
value so far. Each iteration we will compare the next item in the array with the highest
value so far, if it's larger it will replace it and if not, we simply continue without updating the highest
value.
const todos = [
{ name: “dishes”, priority: 2 },
{ name: “laundry”, priority: 3 ),
{ name: “homework”, priority: 1 }
];
let reducer = (highest, todo) => {
return highest.priority > todo.priority
? Highest
: todo;
}
todos.recuce(reduce, {})
// output: { name: “laundry”, priority: 3 }
This example demonstrates an alternative way of using a reducer. What you do each iteration is up to your imagination. The concept is simple, we take an array of items (or objects in this case), perform an operation and reduce it down to a single return value.
The React Hooks Reducer, similar to the JavaScript Arrays reducer, returns the accumulation of something―in our case, React state. Based on all previous and current actions and state modifications that have taken place in the past, our reducer receives a state
and action
as arguments, the state gets reduced (accumulated) based on the action.type
, and we return our new state after running the instructions that match the case for that specific action.type
.
Just like when we cook something up in real life, for instance, a Bordeaux style Bordelaise sauce, we start with many ingredients: butter, shallots, veal, pepper and of course wine. All of these ingredients are combined together in a pan and simmered or (reduced) down. If repeated and given the same steps, using the same ingredients, same amounts, same stove, and same temperatures, we should yield the same result each time. It should be no wonder where they get the name reducer.
An Overview of State and Actions
We are going to build a Todo application. To start out with, we want a todo list to have an initial todo item that simply says: "Get Started."
When we add a new todo item, the process is to first dispatch an action.
This action gets handled by a Reducer function. Our action type is ADD_TODO
and our reducer function switches on a type. When the reducer function notices the type to be ADD_TODO
, it acts on it by taking the old state, spreading that out and appending our new todo item to the end, we then get our new state as the result.
Another action we might create could be COMPLETE_TODO
—or a better name could be TOGGLE_COMPLETE
, since what we actually want to do is toggle that state on and off like a light switch.
In this case, our reducer doesn't add any new items to the list, it modifies one property of an existing todo item. Let's pick up where we left off above. If we started out with one todo that says “Get Started”, and then we add a new todo, “Take a break”, our state should now have two items:
{
id: 1,
name: 'Get started',
complete: false
},
{
id: 2,
name: 'Take a break',
complete: false
}
Notice that each todo has several properties, one of them is an id
. This is a unique key and we use it to target a specific todo, and change one property of that without affecting the rest of the todos property values. The reducer’s job for TOGGLE_COMPLETE
is to update that complete
property from its current value of false
to the opposite value of true
. Once this is done, any changes will be propagated down to any components that use that state, in turn causing them to update.
Since each completed
property in our Todos start as false
, if we call TOGGLE_COMPLETED
on the Todo with the id of 1
, our state should get updated to look like:
{
id: 1,
name: 'Get started',
complete: true // We changed it!
},
{
id: 2,
name: 'Take a break',
complete: false
}
Before Hooks, this type of reducer operation was not easily achievable without a third-party library. Now we can easily implement a reducer pattern in any React application without including another dependency.
This makes working with the internal state even easier than before. This will not replace all usages of Redux, but it gives React developers a clear and concise Redux-style way of managing internal state right away without installing any dependencies.
The State We Manage
Traditionally in Redux, a decision on how to categorize state and where to store it was one of the biggest questions from a beginner's perspective. It's the first question in their Redux FAQ and here is what they say:
There is no “right” answer for this. Some users prefer to keep every single piece of data in Redux, to maintain a fully serializable and controlled version of their application at all times. Others prefer to keep non-critical or UI state, such as “is this drop-down currently open”, inside a component's internal state.
Hooks are powerful in the layer of the application where we keep track of things like “is drop-down open” and “is menu closed.” We can take care of the proper management of the UI data in a Redux-style manner without leaving React core.
In a machine whose sole responsibility is to constantly change and append state, the reducer is the part that is different about each operation. It's the logic that will increment a counter or manages a complex object that changes to have ramifications on the current state. Giving us access to that as well as setState from within functional components is the final piece of the puzzle—and at the same time, the first piece to a new puzzle.
Let's take a look at how we manage the very simple todo type application. It's a perfect example for demonstration purposes. Here are the rules of our Todo app.
We are going to need a few moving pieces to contrive even a simple real-world case for using useReducer. We will need to keep track of how our state is modified and updated using actions like add
, complete
and clear
. Using a pattern familiar to Redux, we typically would associate each of these processes with a particular action type that is handled by a dispatcher:
- A form field allowing us to enter a task
- A dispatcher handling our form when it submits
- An actual object that holds our tasks
- An actual Task Component to encompass everything
- An actual Reducer that handles the modifying of our state
Let's start by adding and composing all of these pieces together. I don't typically go over how to set up a React project because that can be done many ways—instead, I like to give my readers a StackBlitz demo that they can fork and work on alongside the tutorial. Once forked, this project is yours to do with as you want. You can use this information and this code however you want.
Where we start is with a brand new project and for this tutorial, we will do everything inside the index.js
file. Once finished, you may want to make it a point to extract each piece of logic and all components out to their own files. This is a great exercise, especially for beginners.
In our simple example, we will have the Todo component that we create to be the actual root level component of our application, which would look something like the following:
import React from "react";
import { render } from "react-dom";
import "./style.css";
const Todo = () => {
return <>Todo Goes Here</>;
};
render(<Todo />, document.getElementById("root"));
But the code you will start out with is a little bit further along. It's enough to get us started, including a form and input field that does not yet submit. Also added is a styled list and some Json. We can use this for seeding some todo items to test that our list renders, and that the shape of our data conforms to our HTML.
You will need to fork this StackBlitz Demo to follow along with the tutorial. There is a fork button on the StackBlitz demo - once you click it you can give it a new name, and this will create a clone of my starting point for you to work on.
Now that we have the project forked, we will make our first change by importing the useReducer
hook from React. Update the first line in the file as follows:
import React, { useReducer } from "react";
We need to add our call to the useReducer
now. It takes state
and action
as arguments. We assign that to an array object which is a tuple (two values)—this is destructuring because the useReducer()
matches this as its return value:
Add the following line just above the return statement in the Todo component:
const [todos, dispatch] = useReducer(todoReducer, initialState);
todos
will be the piece of state which is the actual list of todo items, and dispatch
will be the actual reducer used to make changes to that list of items. In the return
statement we create a group of divs for each item in our items
array.
Our application will now have errors because we have not yet created a function called todoReducer
. Let's add that code right below the line that we set up our initialState
assignment on.
const todoReducer = (state, action) => {
switch (action.type) {
case "ADD_TODO": {
return action.name.length
? [
...state,
{
id: state.length
? Math.max(...state.map((todo) => todo.id)) + 1
: 0,
name: action.name,
complete: false,
},
]
: state;
}
default: {
return state;
}
}
};
This may seem complex at first. All it's doing is setting up a function that takes state
and action
. We then switch on that action.type
. At first, we will only have one action, but we want to set up a default catch-all as well. This default will simply return the current state.
But if it catches a real ADD_TODO
we will return the current state, spread out, with our payload appended on to the end. The tricky part is assigning the new ID. What we have done here is taken the existing list of todos and returned the max id plus one, otherwise zero.
Since I have already set up an initialState
, we are good to move onto the next step. We need to make sure that when typing in the input field when we hit enter, the value we have input gets sent off to a function that will do the reducing.
So first let's replace the div with the className todo-input
with the following:
<div className="todo-input">
<form onSubmit={addTodo}>
<input
ref={inputRef}
type="search"
id="add-todo"
placeholder="Add Todo..."
/>
</form>
</div>
This ensures that when we hit enter, we send the form information off to a function called addTodo()
. We also reference the input using the ref
attribute and give that element a reference value of inputRef
. Along with these updates we need to do a few more things.
-
- We need to create a property called
inputRef
which calls theuseRef
hook
- We need to create a property called
-
- We need to create a function called
addTodo()
- We need to create a function called
Let's start with creating the inputRef
property. At the top of your todo component, add the following property:
const inputRef = useRef();
We will use the ref attribute to get a reference to the input, which will allow us to access its value later. This reference will be backed by a local property in our todo functional component, but that will just be a call to the useRef
hook. With a local property named inputRef
we will be able to use: inputRef.current.value
to get at that input’s text.
You will also need to import that hook just like we did with useReducer
. Update the first line of the index.js
file to reflect the following:
import React, { useReducer, useRef } from 'react';
Finally, we need to create the addTodo()
function that will use this reference and will be responsible for dispatching our action of type ADD_TODO
. Just above the return
add the following function:
function addTodo(event) {
event.preventDefault();
dispatch({
type: "ADD_TODO",
name: inputRef.current.value,
complete: false,
});
inputRef.current.value = "";
}
Inside of our function, we are first calling preventDefault()
in order to keep the page from refreshing when we hit submit the form.
We then dispatch our ADD_TODO
action using the inputRef
to access the input value from the form. All todos initially get a completed value of false. Finally, we set the inputRef
value to nothing. This clears the input field. Good enough for a demo.
Finally, we have one more update we need to make before the ADD_TODO
will work. Inside of our JSX we are still mapping over initialState
. We need to change that from the line below:
{initialState.map((todo) => (
to:
{todos.map((todo) => (
Now we should have a working useReducer
hook that is utilizing our addTodo
function to dispatch our action
to the todoReducer
.
Adding Completed Todos
Let's have a working example of useEffect
inside this project as well. We will update the document.title
every time that we check off a todo to display the count or number of completed todos in our list.
Just above our addTodo()
function, let's add the logic for figuring out how many completed todos we have. We will then need a useEffect
method to update the document.title
when it changes:
const completedTodos = todos.filter((todo) => todo.complete);
useEffect(() => {
// inputRef.current.focus();
document.title = `You have ${completedTodos.length} items completed!`;
});
To do this we will need to bring in that hook as well:
import React, { useReducer, useRef, useEffect } from 'react';
We are not done yet—we now need to add an event that will call the function which will dispatch our COMPLETED_TODO
. Add an onClick handler to our div
with the className of todo-name
.
<div className="todo-name" onClick={() => toggleComplete(todo.id)}>
{todo.name}
</div>
Next, we need a function to handle this click event. It's simple and only dispatches a simple id and the action type. Add this right below our addTodo()
function:
function toggleComplete(id) {
dispatch({ type: "TOGGLE_COMPLETE", id });
}
Finally, we add the case to our todoReducer
:
case 'TOGGLE_COMPLETE': {
return state.map((item) =>
item.id === action.id
? { ...item, complete: !item.complete }
: item
)
}
I have also set up a style, and we will add or remove that style based on whether the todo has a completed value of true. Just below the todos.map
code, let's change the line of code that looks like this:
<div key={todo.id} alt={todo.id} className="column-item">
to this:
<div className={`column-item ${todo.complete ? 'completed' : null}`}
key={todo.id}>
We don't need that alt attribute anymore, so we removed it. That should do it! Now when we click on our todos it will dispatch an action and set the completed value to true on that specific todo, and now our filter will pick up on this by way of the useEffect
method, which in turn updates the document.title
. We will also get our className completed
applied and our completed todo will become opaque to represent a completed todo.
At this point we pretty much have everything working except for the delete functionality, as well as the button that should clear all todos from the list. To round out our demo we will repeat what we have already learned to make these last two pieces of functionality work.
Deleting a Todo
It should be pretty trivial at this point to hook (pun intended) up to a Delete and Clear todos button. The styling and HTML have already been taken care of, so we just need to make them work.
Let's start by adding the onClick()
event for the close icon inside the todos HTML:
<div className="todo-delete" onClick={() => deleteTodo(todo.id)}>
×
</div>
We'll add the function that will dispatch the action—these don't have to be their own function, we could dispatch right from the onClick()
or we could setup a similar switch statement to handle all of the dispatching. We can take whatever approach to this we want. I wanted to add them one by one for purposes of this demo.
Now we create a function that will handle the dispatch:
function deleteTodo(id) {
dispatch({ type: "DELETE_TODO", id });
}
Now we just add a case in our reducer's switch statement to handle the reduction. This is where we remove a todo from the list using the array;s .filter()
method and return the old state minus the deleted Todo with matching id
.
case 'DELETE_TODO': {
return state.filter((x) => x.id !== action.id);
}
Clearing All Todos
Nothing fancy for the clearing of todos, we are simply going to return an empty array.
Here are the three different pieces of code you will need to make that happen.
Add an onClick()
to the HTML button:
onClick={() => clearTodos()}
Add a function to handle the dispatch:
function clearTodos() {
dispatch({ type: "CLEAR_TODOS" });
}
And a case in our reducer function:
case 'CLEAR_TODOS': {
return [];
}
A Wrap-up on Reducers
We have now worked our way through building the basics of a Todo application using useReducer
. This pattern will be helpful when you are dealing with state that is a little more complex with sub levels of data. We learned about pure functions and why they are the heart of a reducer, allowing us to return state that is predictable, and with this pattern now much easier to do within the core React library.
Chapter 5
Custom React Hooks
Let's learn what it takes to create a custom React Hook as well as all the rules we must keep in mind when using Hooks.
Hooks are just functions! Anything that is a function can become a Hook. I feel that the documentation on the ReactJS docs site is not simple enough. This is no knock on them, I just felt if I could try to explain it in even simpler terms, more people could benefit. So here is my stab at this one!
Revisiting the Effect Hook
If you feel that you have sufficient knowledge of basic Hooks, you can skip directly to creating custom Hooks.
Without going through all the basic Hooks again, I think we just need to revisit one of them: the useEffect
Hook. I learned while reading up on Hooks on the ReactJS.org docs that there are two ways of using useEffect
. You can use it without cleanup or with cleanup. These are terms I expect anyone at this stage of working with Hooks to either know or to take a few minutes to understand with the links I just provided.
With classes and before Hooks were available, side effects were placed in one of many lifecycle methods like: componentDidMount
or componentDidUpdate
. In cases where we have duplicated code in both of those methods (performing the same effect for mounting and updating), we can now do these things inside a functional component and we can do it with just one Hook. That's right, I'm talking about useEffect
.
useEffect
tells React that our component needs to do something after the component renders. It runs after the first render and after every update. In my previous articles, I only talk about side effects WITHOUT cleanup, so I would like to cover really quickly how to allow a functional component to have a side effect WITH cleanup.
Below is an example of how useEffect
can run without any cleanup:
useEffect(() => {
document.title = `You clicked ${count} times`;
});
If you do need cleanup to run, you can return a function from useEffect
. This is optional and it allows you to run some code after your effect and before any new effect runs. A situation where you subscribe to something may need an unsubscribe as part of the effects cleanup process. React will perform this cleanup on unmount.
useEffect(() => {
console.log("Subscribe to Something");
return function cleanup() {
console.log("Unsubscribe to Something");
};
});
The effect above will run on every render more than one time. React cleans up effects from the previous render before running the effects of the next render, and this should be noted. For an explanation on why Hooks run on each update, check out the ReactJS Docs. Remember though, this behavior can be opted out of if it causes performance issues.
We can also optimize performance by skipping effects with an optional argument. For instance, maybe we don't want to run the subscribe/unsubscribe effect unless some id has changed. Check out the example below to understand how this can be done, it's fairly simple!
useEffect(() => {
console.log("Subscribe to Something");
return () => {
console.log("Unsubscribe to Something");
};
}, [props.something.id]); // only if something.id changes
Hooks, and specifically useEffect
, now allow you to split up code based on what it's doing rather than what lifecycle method it's in. When we only had classes and lifecycle methods, we would sometimes have to mix concerns. Now, using multiple useEffect
methods, React can apply each effect in the order they are specified. This is a huge benefit for organizing code in your application.
Creating a Custom Hook
I really liked something that was tweeted out recently by Adam Rackis: "Hooks unleash a level of composition well above and beyond anything we've seen." What I would have you understand about Hooks is that all of the great changes that we have seen with Classes and how we have so many options for composition, well that's all available in Hooks now. This means that now our hands are not tied when it comes to the composition of functional components in React. And this is a huge advancement for React developers.
Custom Hooks are JavaScript functions whose names are prefixed with the word use
. A custom Hook is a normal function but we hold them to a different standard. By adding the word use
to the beginning, it lets us know that this function follows the rules of Hooks.
With a better understanding of Hooks, let's take what we know to be a simple piece of code, our document title update, and create a simple custom Hook.
It seems like something we may want to do on several pages or inside numerous different functional components in our app. When information changes we want to update the document title with some type of string. Additionally, we don't want to repeat this logic inside every functional component. We will start by extracting this code into a Hook locally on the same page, and then see how the same Hook can be imported into many components and co-located. Pretty simple right?
So we know that a Hook can call a Hook. And if that is true then our custom Hook can also call one of the React Core Basic Hooks, like useEffect
. Let's review a functional component that updates the document title one more time. The code below can also be seen in this StackBlitz example.
import React, { Component, useState, useEffect } from "react";
function Counter() {
const [count, setCount] = useState(0);
const incrementCount = () => setCount(count + 1);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={incrementCount}>Click me</button>
</div>
);
}
export default Counter;
So what we would like to do here is to create a custom Hook that we pass a piece of text into and the Hook updates the document title for us. Let's first look just at the code required to create this custom Hook:
const useDocumentTitle = (title) => {
useEffect(() => {
document.title = title;
}, [title]);
};
Above you see that all we need this Hook to take as an argument is a string of text which we will call title
. Inside the Hook we call React Core's basic useEffect
Hook and set the title so long as the title has changed. The second argument to useEffect
will perform that check for us and only update the title if its local state is different than what we are passing in. You mean, creating a custom Hook is as easy as creating a function? Yes, it's that easy at the core, and that function can reference any other Hook. Hot damn... Creating custom Hooks is easier than we thought!
Let's review what our overall functional component will now look like. You will see that I left the old call to useEffect
commented out, above it is how we use the custom Hook for this instead. This can be viewed in an updated StackBlitz demo:
import React, { Component, useState, useEffect } from "react";
const useDocumentTitle = (title) => {
useEffect(() => {
document.title = title;
}, [title]);
};
function Counter() {
const [count, setCount] = useState(0);
const incrementCount = () => setCount(count + 1);
useDocumentTitle(`You clicked ${count} times`);
// useEffect(() => {
// document.title = `You clicked ${count} times`
// });
return (
<div>
<p>You clicked {count} times</p>
<button onClick={incrementCount}>Click me</button>
</div>
);
}
export default Counter;
Let's clean it up just a little bit more and see how we might use this hook if it were supplied by some npm package instead of being copy-pasted at the top of our file. The code below can be found in an updated StackBlitz demo.
import React, { Component, useState } from "react";
import useDocumentTitle from "@rehooks/document-title";
function Counter() {
const [count, setCount] = useState(0);
const incrementCount = () => setCount(count + 1);
useDocumentTitle(`You clicked ${count} times`);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={incrementCount}>Click me</button>
</div>
);
}
export default Counter;
This is fabulous, but I also want you to notice that I don't have to import useEffect
in my functional component, because that gets taken care of in the Hook we imported from '@rehooks/document-title'
. So if I don't need useEffect
, I can omit that from my component imports.
I hope this illustrates the very basics of creating a custom React Hook and that you see the power even with such a simple example.
Here are the two StackBlitz examples side by side if you want to fork them and play around!
The easiest way to discover more React Hooks that you can either copy and paste into your code or npm install is to visit these GitHub related links:
Big thanks to Amit Solanki who made this document title Hook available as an npm package, as well as Adam Rackis for contributing such a profound outlook on Hooks in a brilliant tweet that inspired me to write about the subject.
Chapter 6
New React Hooks
As React changes and evolves, we get more hooks that come as part of new features! This section will be continually updated with new hooks that have been released since this article was published.
New in React 18: useTransition
useTransition
is what allows us to tap into the new concurrent mode features introduced in React 18 – specifically, startTransition
. startTransition
lets us designate specific state updates as "transitions" to tell React that they're non-urgent and can be interrupted by more urgent things (like new user clicks or actions). For a more in-depth look at concurrency and transitions, check out our blog Taking a Look at startTransition in React 18.
So how does this hook get used? It's pretty straightforward! First, you'll need to import the hook and add it to your component. You'll notice that there are two parts to this: isPending
and startTransition
.
import { useTransition } from "react";
const [isPending, startTransition] = useTransition();
isPending
allows you to quickly check whether or not your transition is still rendering. While it's still in progress,isPending
will be automatically set totrue
, which you can then use to determine whether or not you need to adjust your UI to reflect the fact that it's not quite ready yet – for example, disabling a button.<Button className={isPending ? "disabled" : "active"} />
startTransition
is how you set your updates as transitions. You'll just wrap your state updates insidestartTransition
, and then React knows to handle anything inside as less-urgent.onChange = (e) => { const value = e.target.value; startTransition(() => { nonUrgentAction(value); }); };
Just like that, another great hook to add to your repertoire! It's also worth noting that you can start integrating this (and the other React 18 updates) into your work as soon as you like, since it's not a breaking change. All your updates will still be considered urgent by default and render as they did before – unless they're specifically marked as transitions using this approach. So you can start taking advantage of this new hook right away!
Managing Control State of a KendoReact Component
Hooks are great for dealing with certain types of application state. A few examples are control state, local component state and session state. I'd like to leverage Hooks when working with our KendoReact UI components, but I want to start simple. We will refactor one of the StackBlitz demos from using classes and instead use a functional component. We will look for instances where the demo is using this.state
and this.setState
because when we convert the component to functional we will not have to use the this
keyword anymore, we will not need to use a constructor or call setState
. So let's get into refactoring the KendoReact demo that shows how to work with our KendoReact Dialog. I have forked the original StackBlitz demo from the Dialog Overview page, and that will be our starting point.
If you look at this demo's main.jsx
page which I have shown below, there are several target areas we can identify that will change when using a functional component and React Hooks. The code and lines highlighted in GREEN will need modification, and the lines highlighted in RED will be able to be removed completely.
- On line 6 we have a
class
definition, we need to convert this to a functional component. - On line 7 we have a constructor, on line 8 a call to
super()
and on line 10 we have some binding. None of these are needed in our functional component using Hooks. - On line 9 we create an instance of our state and give it a default value of
true
, this will instead be a call to theuseState
Hook. - On line 13 we need to rename the
toggleDialog
function and switch it to the ES6 Arrow Function style syntax. Lines 14 through 16 will simply call an update method provided by ouruseState()
assignment calledsetVisible
, and the value it will be referencing will bevisible
instead ofthis.state.visible
. - On line 19 we have a call to
render()
which will not be necessary in our functional component. - On lines 22, 23, 26 and 27 we have references to
this.
andthis.state
that will need to be to referencevisible
andtoggleVisible
instead oftoggleDialog
, and I will explain later on why I want to rename that function.
The first thing we need to do is to convert the class to a functional component, then remove the constructor with its call to super()
and its binding of the toggleDialog()
function. There are multiple syntax options we could use here, I prefer the ES6 Arrow Function style:
const multiply = (x, y) => {
return x * y;
};
In our component line 6 would now look like this:
const DialogWrapper = () => {
Let's go ahead and set up our Hook that will take the place of the state object. Instead of creating an object named state, we will set up a call to useState()
and destructure its return value into a variable that will hold our state and an update/set method to update that piece of state. Our name of our state will be visible
and its update method will be called setVisible
. We will remove the entire constructor and replace it with this one line:
const [visible, setVisible] = useState(true);
Since we are using the useState()
basic Hook, we also need to import it. Our React import will now look like:
import React, { useState } from "react";
Next, we need a function inside this component that calls setVisible
to toggle its value. We name it toggleVisible
instead of toggleDialog
and since we are in a functional component, the syntax that was used before will not work. Instead, I will update it to the ES6 Arrow Function style. This function will simply set the visible
state to the opposite of its current state at the time.
const DialogWrapper = () => {;
const [visible, setVisible] = useState(true);
const toggleVisible = () => setVisible(!visible);
Now we need to get rid of the render()
block and its two curly braces. Also, we need to remove all references to this this.toggleDialog
and this.state.visible
and change them to toggleVisible
and visible
accordingly. Now inside of our return()
we will have the following changes:
return (
<div>
<Button className="k-button" onClick={toggleVisible}>
Open Dialog
</Button>
{visible && (
<Dialog title={"Please confirm"} onClose={toggleVisible}>
<p style={{ margin: "25px", textAlign: "center" }}>
Are you sure you want to continue?
</p>
<DialogActionsBar>
<Button className="k-button" onClick={toggleVisible}>
No
</Button>
<Button className="k-button" onClick={toggleVisible}>
Yes
</Button>
</DialogActionsBar>
</Dialog>
)}
</div>
);
Again we have just updated our code in the return()
to not reference the this
keyword and to use our new function name toggleVisible
.
We have successfully converted our KendoReact demo to use a functional component and the basic useState
hook. Let's take a look at what our overall changes looked like using an awesome tool called GitHistory:
What I have done here is downloaded the original StackBlitz class-based demo into its own Git repo. The class-based version would be the initial commit and then I made a second commit after refactoring the component to a functional hook. GitHistory gives me the ability to scrub through each commit in our main.jsx
file, animating its changes over those two commits. I think it's a powerful visual for learning how to use Hooks and seeing the code changes from the old class-based approach to the function-based approach.
I have also pushed this repo to my GitHub account where you can view it with GitHistory on your own. GitHistory, created by Rodrigo Pombo, is a very cool plugin that allows you to diff any file in your repo in the manner we saw above.
Conclusion
I hope this guide aids you in better understanding the basics of Hooks, and will allow you to build on these examples and create new and amazing things. If it has been useful to you, please share and spread the word.
I want to leave you with a great understanding of why Hooks were created and I think this can best be explained by looking back to Sophie Alpert's talk at React Conf 2018.
In the past, some React developers have experienced confusion around when to use and when not to use classes. This question can be dated back years ago in one case by an article from Dan Abramov: How to Sleep at Night using React Classes. Even though we might sometimes use them in the current React or run across them in the future when working with legacy code, that issue is being handled now and we already see developers having strong opinions and using mostly functional components.
When talking about what the React team was doing to make it easier to build great UIs as well as improve the developer experience in React, Sophie Alpert asked a great question.
What in React still sucks?
Here are the answers from that now-famous talk at React Conf 2018:
Reusing Logic
Before React Hooks, we used a lot of higher-order components and render props to achieve this, and this would require you to restructure your app often when using these patterns and leads to wrapper hell (pyramid of doom style nesting).
Giant Components
We often had a tangled mess in our components because of different pieces of logic split amongst different lifecycle methods.
Confusing Classes
This is where I get to our first of many quotes that I will leave you with.
Classes are hard for humans, but it's not just humans, classes are also hard for machines -- Sophie Alpert
Understanding classes in JavaScript can be tricky, as well it was a requirement before Hooks that you had to use class components to access state and lifecycle methods. There is a fair amount of boilerplate required to simply define a class component. Hooks help to resolve these issues and for this reason, I want to leave you with some other notable quotes from our fearless React and community leaders!
Hooks let you always use functions instead of switching between functions, classes, HOCs, and render props -- Dan Abramov
If you wanna make the world a better place, take a look at React Hooks, then make that change. -- Michael Jackson
Hooks offer a new way to approach problems in React -- Dave Ceddia
With React hooks, we have the best of both worlds: clean functional components that can use state -- David Katz
The Hooks are React, it's where React is going, it is React V2 -- Michael Jackson
React Hooks radically simplified how I create, write, read and prototype components -- Zach Johnson
These are just some things people are saying about React Hooks. Do you have something to say about your experience with Hooks? Let me know by leaving a quick comment below.
Links and Resources
All of the amazing links that I used to learn about Hooks and the resources I believe to be important as a companion to this article.
React JS Documentation Links
- Introducing Hooks
- Hooks at a Glance
- Using the State Hook
- Using the Effect Hook
- Rules of Hooks
- Building Your Own Hooks
- Hooks API Reference
- Hooks FAQ
Other React JS Resource Links
Important Article Links
- Hooks at a Glance
- Making Sense of React Hooks
- How Are Function Components Different from Classes?
- A Complete Guide to useEffect
- Why Do React Hooks Rely on Call Order?
- Why Isn’t X a Hook?
- Making setInterval Declarative with React Hooks
- Understanding Hooks in React a Deep Dive
- A Simple Intro into Hooks
- Hooked (Formiddable)
Video Resources on Hooks
- React Conf 2018 Day One Talks
- Share Complex Logic across React Components with Custom Hooks
- Access and Modify a DOM Node with the React useRef and useEffect Hooks
- Share Logic Across Multiple React Components with Custom Hooks
- Use the useState React Hook Test React Components that use React Hooks
- React Hooks a Complete Introduction
- TODO List with Hooks
- State Management with React Hooks
About the author
Eric Bishard
Eric Bishard is a Developer Advocate working with KendoReact here at Progress. As a software engineer, he has experience building web based applications with a focus on components for user interfaces in frameworks such as React and Angular.