In the next post in our series on React Hooks, we'll learn how to use reducers by walking through a sample todo app. Get "hooked" as we take a look at the useReducer React Hook!
In the past few articles, we have become familiar with React Hooks. In our first article, we learned about State & Effect which allows us access to local state and side effects inside a functional component.
In our second article we learned about React's Context API and demonstrated how to use it with regular classes in a profile component. The Context API allows us to share data with child components using a provider and consumer. But until Hooks came around this was not achievable with functional components. So we refactored with the useContext
hook. This involved changing classes in our project to functional components eventually removing all classes from our demo making everything easier to read with less syntax. In both cases we learned hooks gave us the ability to migrate our code from classes to functional components as Hooks can only be used inside functional components or other hooks.
In this article, we 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 have not read the first article of this series it is highly encouraged before moving on.
If you have been programming React under a rock or you have just taken up React, Redux is the more popular way of working with unidirectional data in React and is encouraged by the React team as well. It has become a standard in the React community and ecosystem, and that's why React has built useReducer
into React, which gives you a way to use the Redux pattern without depending on the Redux library just to do simple UI state management.
In React, Your Only Job is to Manage State
Well, not completely, and I agree this heading is a bit baity. But it's many developers opinion that the main things you should always consider when building React applications is to understand what your state currently looks like and what your UI looks like. This will drive how you manage its state and build your application around it.
Don't worry, we do use separation of concerns in a proper React application, so the state vs UI are kept compartmentalized. We can typically just build out UI and then focus on managing its state. This determines what we do next and we get to decide exactly what happens each time the UI is interacted with and how that might mutate the state we are managing.
A Primer on Reducers
Let's talk about the difference between a Redux state reducer and the JavaScript method Array.prototype.reducer.
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 value summing up all values in the array and returning the total as a single value. The reducer can be fed an initial value to start with as well. Let's briefly take a look at some code that demonstrates the reduce method from JavaScript's Array.prototype
method called reduce()
.
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
If you have worked with Redux reducers, you probably see a similarity in the example I just explained and how Redux reducers work. You can easily see why the Redux Reducer shares a similar name.
A sum function is the simplest example, but inside that reducer, you can do any work you want iteratively between those curly braces. Think of it as a recipe that no matter what the contents of the array, this function always produces the same result considering the same input. A pure function. This is a powerful concept especially when used to manage state for an application.
The Redux Reducer, similar to the Arrays reducer, returns the accumulation of something - in our case 'state' based on all previous and current actions and state modifications that have taken place in the past.
So Redux style reducers act as a reducer of state. It receives a state
and action
. The state gets reduced (accumulated) based on the action type and then returned as the new state. Each time we reduce we can refer to that operation as a single cycle.
Just like in cooking 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. Repeated, given the same steps, using the same ingredients, same amounts, same stove and same temperatures, we should yield the same result each time. A single wonderfully awesome sauce. Still wondering where they get the name reducer?
Let's Build a Working Example
We are going to build a Todo application. To start out with, we want our 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 get's handled by a Reducer function. Our action type is ADD_TODO
and our reducer function switches on these types. When the reducer function notices type ADD_TODO
, it acts on it by taking the old state spreading that existing state out and appending our new Todo item to the end, then returning the new state as the result.
Another action we might create could be COMPLETE_TODO
or better yet, TOGGLE_COMPLETE
. I like the latter as it gives us the ability to incomplete, so to speak, if the user clicks the wrong Todo.
In this case our reducer doesn't add any new items to the list, it modifies one property of an existing Todo item. So 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 new state should look like this:
{
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, an id
. This is a unique key and we use it specifically to know which Todo we want to update.
The reducers job for TOGGLE_COMPLETE
is to update that complete
property from it's 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 this piece of state, causing them to update. Our list can be thought about as the listener, and once we add or complete or remove a Todo, the list should immediately reflect those new changes.
So, since each completed
property in our Todo starts out as false
, if we call TOGGLE_COMPLETED
on the Todo with the id of 1
, our state should get updated to look like the following:
{
id: 1,
name: 'Get started',
complete: true // We changed it!
},
{
id: 2,
name: 'Take a break',
complete: false
}
We have just described (albeit in a simple example) the entire Redux cycle, also referred to as unidirectional data flow.
This was not easily achievable in React without a library like Redux. But now, thanks to the advent of Hooks, we can easily implement the Redux reducer pattern in any React application without using a library like Redux.
I will admit that using state in this manner should probably be reserved for working with internal state, and that this will not mean the large applications where state is managed by Redux are now obsolete. That is not the case here. 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 Will be Managing
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 actually the first question in their Redux FAQ and here is what they state:
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 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 either increments 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 in order 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 the 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 setup a React project because that can be done many ways - instead I like to give my users a StackBlitz demo that they can fork and work on along side 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 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 I want you to start out with is a little bit further along than that. I have added enough to get us started, including a form and input field that does not yet submit. I have also added a styled list and a chunk of Json, which we can use as a seed for generating Todo items to test that our list renders out 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);
items
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 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 setup 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 setup 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 setup 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>
What this does is ensure 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
. Making these changes means we need to now do two more things.
1) We need to create a property called inputRef
which calls the useRef
hook.
2) We need to create a function called addTodo()
.
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, this 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 which allows us to access all of the ref goodness inside a functional component. With a local property named inputRef
we will be able to make calls like: inputRef.value
.
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:
{initialState.map((todo) => (
to:
{todos.map((todo) => (
Now we should have a working useReducer
hook that is utilizing our addTodo
function in order to dispatch our action
to the todoReducer
.
Adding Completed Todos
Let's bring in a familiar hook from our first blog post: useEffect
. I just want to make sure we have a working example of that hook inside this project as well, so 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 a 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 setup 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 in order 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 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 });
}
And you guessed it, we now just need to add a case in our reducers switch statement to handle the reduction, this is where we actually remove a Todo from the list and return the old state minus the deletion. We can do this easily because we have provided an id
. In the same way we know which Todo to complete by passing an id
, we can also delete an item, we just need to ensure that it returns the new state. We can achieve this with the array filter prototype.
case 'DELETE_TODO': {
return state.filter((x) => x.id !== action.id);
}
Clearing All Todos
For the clearing of todos, we actually don't need much. When the action gets dispatched for the CLEAR_TODOS
, the only thing being passed is the actual action type, no payload, because once we get into the reducer, 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 [];
}
Wrap Up
We have now worked our way through building the basics of a Todo application. We started with the basic structure of the Todo list and how to simply repeat our a list for each Todo, and eventually created all of the logic to manage the state of the list using a Redux style approach to state management. We covered adding a new Todo, deleting, completing a Todo and finally the ability to reset the entire Todo list (the easiest part of the reducer to complete).
Look out for more articles from us that will continue our journey into Hooks and advanced Redux concepts. We have covered the basics so far and plan on showing you how to continue to take advantage of all the wonderful ways that you can utilize React Hooks and other bleeding edge features of the framework.
I hope that this has helped you understand the basics of using React Hooks for reducers. If you are new to React, we have more content here on the Telerik blog specifically around All Things React, which contains a plethora of information about React and its ecosystem. Please explore our articles and products and let me know if you have any questions or ideas for articles on subjects relating to React.