useReducer is quite an advanced hook that beginners often don’t really know how to use. But this hook can be very useful for writing better and clearer code!
If you’re not sure of what useReducer does, read on to learn more!
Table of Contents
What is the useReducer hook?
You can think of useReducer as a “state” hook, like useState. It’s a hook that allows you to manage the state of your component. The way the hook works is quite different from useState though.
TIP: If you’d like a refresher of what the useState hook does and how it works, check out this article on it!
Let’s first see a code example of the hook in action, then we’ll explain what is going on.
How to use the useReducer
hook?
The following is a simple counter example taken from React’s documentation. Can you guess what it is doing?
const initialState = {count: 0};
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}
Did you guess it? Congrats if you did! If not, no worries, that’s what you’re here for!
The hook’s arguments
First off, you call the hook with two arguments. The first one is a function called a reducer, and the second one is the initial state.
In the case of the counter above, the initial state is simply an object containing a “count” property initialized at 0: {count: 0}
.
The reducer is a function that takes two arguments: a state and an action. The state is the current state. For example, at the beginning the state has the value {count: 0}
. You can think of the action as a keyword that will tell the function what to do.
NOTE: In reality the action is an object with a “type” property that tells the function what to do. You can see that in the example above. But more of those details later!
In the above example, if the action is decrement it means “decrease the counter by one”. If it’s increment, you can guess what it does!
So the reducer takes the current state, and an action that tells it what action (wink wink) we want to do. The reducer does the action, then returns the new state.
In this state, the action is either add or remove one to the counter.
That’s the arguments! Let’s see what useReducer is returning now.
The hook return object
As you can see in the code, the useReducer hook returns two things: the state, and a function called dispatch. This is pretty similar to useState, which also returns the state and a function to modify the state.
const [state, dispatch] = useReducer(reducer, initialState);
The main difference with useState is in the way you use the function to modify the state. While with useState you use the modifier function to modify the state directly, with useReducer you pass an action to the dispatch function.
That action is precisely the one that is then given to the reducer, to let it know what to do. The convention is for the action to be an object with a “type” property that describes the action.
In the example code above, the action to increment the counter was simply the following object:
{type: 'increment'}
Optionally, you can also add some more information to perform the task.
For example, you could imagine having a new action called incrementBy that would also have a number property telling the reducer how much to increment the state. This action would look something like this:
{type: 'incrementBy', number: 3}
If you don’t get it the first time you read it, don’t worry! Take your time and read the code and the explanation again. Reducers can get pretty complex, so having a good understanding of the basics is key to using them effectively!
When to use useReducer vs useState?
For most of the use cases you’ll want to use useState. It’s the simplest and most convenient option. Where useReducer really shines is when you start having a complex state that is using a lot of different useState.
For example, in a situation like this:
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [password, setPassword] = useState('');
const [repeatPassword, setRepeatPassword] = useState('');
const [email, setEmail] = useState('');
It can go on and on and on… And you end up a bit overwhelmed by having to manage a lot of different state.
useReducer allows you to bring all of that state into one object which is centrally managed. For example:
const initialState = {
firstName: '',
lastName: '',
password: '',
repeatPassword: '',
email: '',
};
function reducer(state, action) {
switch (action.type) {
case 'changeValue':
return {...state, [action.field]: action.value};
default:
throw new Error();
}
}
With the above reducer, changing a field is simply a matter of calling the dispatch method with the right field and the new value. For example if I want to change the firstName field to the new value “Upmostly”, I can do it with the following call:
dispatch({
type: 'changeValue',
field: 'firstName',
value: 'Upmostly'
});
TIP: Reducers often come with a lot of object and array manipulation to modify the state. This is a good time to take a refresher on that subject, for example on how to filter arrays!
Pretty easy right? Having all that state centralized is also pretty convenient if you want to add new action types that act on all of the data. For example, let’s say you want to reset every field to its initial value. In the useState example you’d have to write a function that calls every setState to reset the field: setFirstName(”), setLastName(”), and so on.
With the reducer, you can just add a new type of action called reset and it just works!
const initialState = {
firstName: '',
lastName: '',
password: '',
repeatPassword: '',
email: '',
};
function reducer(state, action) {
switch (action.type) {
case 'changeValue':
return {...state, [action.field]: action.value};
case 'reset':
return initialState;
default:
throw new Error();
}
}
Looks like magic, isn’t it?
Reducers in the Redux library
Reducers are a very central concept in the Redux library. If you haven’t heard of it, Redux is a library that allows you to manage the state centrally in your application. It’s very popular in large and complex apps.
Without getting into too many details (trust me, there’s a LOT to say on Redux!), the way the whole library works is through reducers and actions! Just like in our useReducer, you have an object which contains the state, and that you can modify through actions.
If you decide to learn more about Redux, be sure to understand reducers well!
Wrap up
Hopefully this article has helped you understand the useReducer hook better. It can be tricky to get right, but as we saw in our last example, when it shines it shines brightly!
Some parting words: now that you know how to use reducers, don’t go using them everywhere! Most of the same, your usual useState is more than enough! Start with that as the default, and only if you’re starting to have issues with it think about whether those issues would be solved (or at least alleviated) by using useReducer.
Good luck!
š¬ Leave a comment