So you've been using useState effectively and slowly eliminating all of your class components. But now your functional components are starting to bloat with all your calls to useState. Maybe your code is looking a little like this?
const [userName, setUserName] = useState('');
const [password, setPassWord] = useState('');
const [email, setEmail] = useState('');
const [employmentStatus, setEmploymentStatus] = useState('Student');
const [community, setCommunity] = useState('');
const [loggedIn, setLoggedIn] = userState(false)
useState is great when you only have a couple things to keep track of, but it starts to get out of hand once you have a bunch of things to track. Sure you could set your state to an object and just update the field you want to change... Maybe you've done something like this?
const state [state, setState] = useState({username: '', password: '', email: ''});
setState({...state, username: 'Daniel'});
But that's gonna start to break down when you have nested objects, and we want to get fancy and keep our code tight. So let's talk about useReducer. If you've ever used React Redux, some of what we're about to do might look very familiar, but if not, don't worry about it. useReducer is incredibly similar to using reduce in plain old regular Javascript.
Let's start with a simple example that would probably be easier to solve with useState, but for this example it's going to show us how useReducer is working.
We'd like to make a counter increment with useReducer.
import React, {useReducer} from 'react';
const simpleCounter = () => {
const [counter, dispatch] = useReducer(reducer, 0);
return (
<div>
Counter: {counter}
<span onClick={() => dispatch('increase')}>Increase</span>
<span onClick={() => dispatch('decrease')}>Decrease</span>
</div>
);
};
const reducer = (oldValue, action) => {
switch(action) {
case 'increase': return oldValue + 1;
case 'decrease': return oldValue - 1;
default: return oldValue;
}
};
First thing's first, we just need to import useReducer from react, we could simply call React.useReducer, but instead let's have fun destructuring.
Then we need to call useReducer. It takes a reducer function and an initial value as its parameters, and it returns an array, that we're destructuring, and that contains the current state and a dispatch function.
The dispatch function will call the reducer with two parameters. The first will be the current state, and the second will be whatever arguments you pass into the dispatch function. It's a good convention to call this second parameter action.
So here, our reducer function just takes the old value of the counter as its first parameter and the action you want to take on it as your second parameter, in this case that's 'increment'
or 'decrement'
and it then returns a new value for counter. Remember, Javascript's reduce always has to return the value that it is accumulating, so the same is true here. Whatever you return will be the new state, or in this case, the counter variable.
Ok that example was a little simple so let's step it up just a little. This one is also going to be an overly complex solution for a very simple problem, but let's roll with it because it is going to start looking like something you'll actually see in the real world.
Let's assume we have some more variables in our initial state, other than just the counter, so we can't just return the new counter in our reducer.
const initialState = {
counter: 0,
//...more variables we don't want to overwrite
};
const complexCounter = () => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
Counter: {state.counter}
<span onClick={() => dispatch({type: 'increase'})}>Increase</span>
<span onClick={() => dispatch({type: 'decrease'})}>Decrease</span>
</div>
);
};
const reducer = (prevState, action) => {
switch(action.type) {
case 'increase': return {...prevState, counter: prevState.counter + 1};
case 'decrease': return {...prevState, counter: prevState.counter - 1};
default: return prevState;
}
};
There's only two difference here, one is that instead of just passing in a string of 'increase'
or 'decrease'
, we're passing in an object that has a type of what we want to do. This is a pattern that you're going to see if you start using Redux, but don't worry too much about that, just know that it's a good idea to get used to seeing this pattern associated with reducers.
The other change is that now we aren't just returning the counter variable from our reducer, we're returning a whole object. So we use the spread operator to get a copy of the old state and then overwrite the variable we want to change, in this case the counter.
This works fine for this example, but it's going to become increasingly complex and hard to use as we nest objects inside objects. So in a practical example it would probably be better to update the variable you want to change, and then just return the state with that updated value changed. So that would change our reducer to look something like the example below.
For this example let's assume the counter is an object that has a value key where the value of the counter is stored.
const reducer = (prevState, action) => {
const updatedCounter = {...prevState.counter};
switch(action.type) {
case 'increase':
updatedCounter.value++;
break;
case 'decrease':
updatedCounter.value--;
break;
default: return prevState;
}
return {...prevState, counter: updatedCounter}
};
Here we've created a copy of the counter variable in the state, and we just increment or decrement our new variable's value. Then we can return the updated counter and not have to worry about nested values getting overwritten.
This reducer obviously wouldn't work the way it is currently built for anything other than changing a counter, but I believe it gets the idea across. If you want to see how useReducer works in a practical application, check out how to use it in a form in this series of articles.
Top comments (0)