React offers many ways to manage state. I have previously written about one such method, using redux. Another way to manage react state is through the use of the useReducer hook. In this article, I am going to demonstrate the usage of this hook along with some of its benefits.
The problem with redux
If you haven’t read my article about setting up redux in react, I urge you to read it in order to get some context about what will be discussed in this article.
One of the main complaints against redux is that it requires a lot of boilerplate code to set up some fairly simple functionality. Having to include redux and react-redux increases the project’s bundle size. While the setup increases the complexity of the code.
This is through no fault of the redux developers. Redux is designed to be a general state management tool not exclusive to react. As a result, adapting it to any particular framework will always take a little more setup than something designed specifically for that framework.
Redux also has quite a steep learning curve for some beginners as it introduces paradigms that are hard to grasp. I’m not ashamed to say that it took me at least a couple of weeks of tinkering with redux before I felt comfortable with it.
The complexity of redux is justified for large projects. As the state becomes large and complex enough, the elaborate redux setup eventually pays for itself in such scenarios.
However, there are some projects that are not quite large enough to justify using redux but contain state too complex to manage using the much simpler useState hook. This is where useReducer comes in.
How useReducer solves this problem
useReducer
is a react hook that offers the basic functionality of state management that comes with redux, without all the boilerplate code in the setup.
For projects that have a need for a more sophisticated state management system but do not need the extra bells and whistles that come with redux, this is the (almost) perfect alternative.
Because useReducer
is designed specifically for react, it is extremely easy to integrate into react components.
There are more issues that are addressed by the useReducer
hook. I will discuss these later on in the advantages section of this article.
Using useReducer
Alright, enough talk, time to code! Here’s a demonstration of useReducer in action. For the sake of simplifying this tutorial, I have all the code written inside the App component.
Feel free to break the code down into separate components wherever you see fit. This will work regardless.
We are going to be using a functional component as react does not allow us to use hooks in class components. Make sure to import the useReducer hook:
import React, { useReducer } from 'react';
Now, let’s make use of the hook:
const reducer = (state, action) => {
switch (action.type) {
case 'ADD_LANGUAGE':
return { ...state, languages: [...state.languages, action.payload] }
case 'ADD_FRAMEWORK':
return { ...state, frameworks: [...state.frameworks, action.payload] }
case 'REMOVE_LANGUAGE':
return { ...state, languages: state.languages.filter( (language, index) => index !== action.payload ) }
case 'REMOVE_FRAMEWORK':
return { ...state, frameworks: state.frameworks.filter( (framework, index) => index !== action.payload ) }
default:
return state
}
}
const initialState = {
name: 'Kelvin Mwinuka',
occupation: 'Software Developer',
languages: ['JavaScript', 'Python'],
frameworks: ['React', 'Flask', 'Express']
}
const [state, dispatch] = useReducer(reducer, initialState)
If you’ve used redux before, a lot of this looks very familiar. In fact, the useReducer
hook is basically redux lite.
First, we set up our reducer. This takes the current state and the dispatched action as parameters. Depending on the action type, we return the current state with the relevant data (payload) added to it.
Next, we set up our initial state. This can be an empty object. I’ve put some data in the initial state here because I’d like something to be displayed on the first render. If you do not need this behaviour, feel free to leave this empty.
Finally, we initialise state and dispatch using the useReducer
hook. The 2 primary arguments are the reducer and initial state.
We will access state when displaying information while rendering but use dispatch in order to update the state.
Now let’s render the visual elements that will allow us to interact with our state:
return (
<div className="App">
<div>
<p><b>{state.name} </b>({state.occupation})</p>
<h3>Languages</h3>
<ul>
{state.languages.map((language, index) => {
return (
<li key={index}>
<b>{language}</b>
<button onClick={() => { dispatch({type: 'REMOVE_LANGUAGE', payload: index})} }>
Remove
</button>
</li>
)
})}
</ul>
<form onSubmit={handleSubmit}>
<input type='text' name='language' />
<input type='submit' value='Add Language' />
</form>
<h3>Frameworks</h3>
<ul>
{state.frameworks.map((framework, index) => {
return (
<li key={index}>
<b>{framework}</b>
<button onClick={() => { dispatch({type: 'REMOVE_FRAMEWORK', payload: index})} }>
Remove
</button>
</li>
)
})}
</ul>
<form onSubmit={handleSubmit}>
<input type='text' name='framework' />
<input type='submit' value='Add Framework' />
</form>
</div>
</div>
)
Here we create 2 lists that will display our languages and frameworks respectively. Each list has a corresponding form that allows us to add to it. Additionally, each list entry has a delete button that allows us to remove that particular item from its list.
Let’s start with the delete buttons as they have the simplest logic. Each delete button rendered is aware of its index in the list. When clicked, the button dispatches an action that has a type and payload (just like redux).
The payload is the index of the button/item. So how does the reducer know which list to remove from?
Well, the delete buttons in the languages list dispatch an action with type REMOVE_LANGUAGE
. As you can see, the reducer listens for this specific action and then deletes the given index in the payload from the languages list.
The delete buttons in the frameworks list dispatch a similar action except they pass a type of REMOVE_FRAMEWORK
. The reducer also listens for this type of action and responds by filtering out the item at the index passed in the payload.
Now let’s handle adding to the lists.
Both forms have the same submit handler. Let’s define this within our app component:
const handleSubmit = (event) => {
event.preventDefault()
const formData = new FormData(event.target)
const language = formData.get('language') // Returns null if 'language' is not defined
const framework = formData.get('framework') // Returns null if 'framework' is not defined
const action = language ? {type: 'ADD_LANGUAGE', payload: language} :
framework ? {type: 'ADD_FRAMEWORK', payload: framework} : null
dispatch(action)
event.target.reset()
}
Here we capture the form submit event (for both forms). We then create a FormData
object from the form. Next, we capture the language and framework value from the FormData.
The language key will return null for the framework form and vice versa.
We then use nested ternary operators to determine what the action object should look like. The payload is the same for both forms, a string.
However, in order for the reducer to know which list to append the string to, we need a type of ADD_LANGUAGE
in the case language is not null, and a type of ADD_FRAMEWORK
when framework is not null.
Finally we dispatch the action we’ve just created and reset the target form.
Working with child components
So the next question is: how do we work with child components?
In redux, we can pass in the relevant portion of the state down to child components along with actions. We can also directly connect each component to a relevant section of the state using mapStateToProps. Action creators can be mapped to props using mapDispatchToProps.
With useReducer
, we need not pass anything other than the relevant portion of state and the dispatch function itself for action dispatching.
Let’s look at an example of this.
First, we will separate the languages and frameworks sections into their own components:
const Languages = ({ languages, handleSubmit, dispatch }) => {
return (
<div>
<h3>Languages</h3>
<ul>
{languages.map((language, index) => {
return (
<li key={index}>
<b>{language}</b>
<button onClick={() => { dispatch({ type: 'REMOVE_LANGUAGE', payload: index }) }}>
Remove
</button>
</li>
)
})}
</ul>
<form onSubmit={handleSubmit}>
<input type='text' name='language' />
<input type='submit' value='Add Language' />
</form>
</div>
)
}
const Frameworks = ({ frameworks, handleSubmit, dispatch }) => {
return (
<div>
<h3>Frameworks</h3>
<ul>
{frameworks.map((framework, index) => {
return (
<li key={index}>
<b>{framework}</b>
<button onClick={() => { dispatch({ type: 'REMOVE_FRAMEWORK', payload: index }) }}>
Remove
</button>
</li>
)
})}
</ul>
<form onSubmit={handleSubmit}>
<input type='text' name='framework' />
<input type='submit' value='Add Framework' />
</form>
</div>
)
}
Now that we’ve extracted this code into separate components, we can update the App component’s JSX:
return (
<div className="App">
<div>
<p><b>{state.name} </b>({state.occupation})</p>
<Languages languages={state.languages} handleSubmit={handleSubmit} dispatch />
<Frameworks frameworks={state.frameworks} handleSubmit={handleSubmit} dispatch/>
</div>
</div>
)
If we want to update the state from our child components, all we need to pass down is the dispatch function. The chid component will be responsible for dispatching the appropriate action in its logic.
This prevents having to pass multiple functions and callbacks, which can quickly become overwhelming.
Advantages of useReducer
Now that we’ve seen how to implement useReducer, let’s discuss why you should use this hook:
1. Simplicity
The first reason is one we’ve already discussed before, it is simple. This hook removes all the boilerplate associated with redux. This is invaluable for projects that aren’t large enough to justify the use of redux.
2. Handle more complex state than useState
If your application’s state has multiple levels, using the useState hook can become very tedious. To combat this and achieve a clean state management solution, the useReducer hook is more suitable for the task.
3. Reduces obnoxious prop-drilling
One of the ways in which we update state from child components is using a technique called prop drilling.
This is a technique whereby a callback function is passed down multiple levels until it reaches the relevant component that uses it.
Technically, we’re still prop-drilling the dispatch function through all our components.
However, the dispatch function is potentially relevant to all the components it passes through as it is component agnostic.
4. Removes external library dependencies
Redux is an external library, and therefore adds to the external dependencies of your react project.
If you are conscious about this due to concerns about bundle size or any other reason, then useReducer is a perfect way to manage some fairly complex state without having to rely on an external package.