Exploring the Efficiencies of useReducer in React: A Comparative Analysis of State Management (part-1)
This article will give a general understanding of how to implement useReducer in React.
Introduction
I previously favored useState
for managing state in React applications, however, I have recently implemented useReducer
and have found it to be a more efficient method in comparison to the traditional useState
. In specific situations, useReducer
has proven to be the optimal solution for managing state. One of the key benefits of using useReducer
is its effectiveness in managing global state within large React applications. This is particularly useful when combined with the useContext
hook.
In Part 2, I will demonstrate the benefits of combining useReducer
and useContext
for managing state in React applications, as well as provide a comparison with useState
.
This article aims to provide a comprehensive overview of the use of useReducer in React, including a comparison of its functionality and benefits in relation to the traditional useState hook. So let's get hooked, shall we?
Before diving deeper into the topic, it is crucial to understand the concept of a reducer.
What is reducer?
Reducer is not a concept specific to React, rather it is a concept of JavaScript. To understand it, let's explore the built-in method in JavaScript called Array.prototype.reduce(). Its purpose is to reduce an array into a single value by taking a reducer callback function and an initial value as arguments. Let's see the code below:
//define the reducer function
const reducer = (accumulator, currentValue) => accumulator + currentValue;
//Now use this reducer as a callback in Array.prototype.reduce()
const array1 = [1, 2, 3, 4];
let initial_value = 0;
const sum = array1.reduce(reducer, initial_value);
console.log(sum); //10
initial_value = 5;
const sum2 = array1.reduce(reducer, initial_value);
console.log(sum2); //15
See mdn web docs for better understanding of Array.prototype.reduce()
.
Once you have a grasp on the concept of a reducer, you are ready to proceed with understanding the use of useReducer in React.
What is useReducer?
It is an important hook in React for state management, it allows you to add a reducer function to your component. Both the Array.prototype.reduce()
method and useReducer
have similar purposes, which is to reduce a collection of values into a single value by taking a reducer function and an initial state as arguments and applying the reducer function to each value in the collection to produce the final reduced value.
Now, let's explore the similarities and differences between the JavaScript's reduce()
method and React's useReducer
hook :
reduce | useReducer |
array.reduce(reducer, initialValue) | useReducer(reducer, initialState) |
reducer(accumulator, currentValue) | reducer(currentState, action) |
returns a single value | returns an array = [state, dispatch] |
Let's have a look at the implementation of useReducer
in a simple react component :
import { useReducer } from 'react';
const initialState = 0;
const reducer = (state, action) => {
switch (action) {
case 'increment':
return state + 1;
case 'decrement':
return state - 1;
default:
return state;
}
}
const Counter = () => {
const [count, dispatch] = useReducer(reducer, initialState);
return (
<>
<p>Count: {count}</p>
<button onClick={() => dispatch('increment')}>
+
</button>
<button onClick={() => dispatch('decrement')}>
-
</button>
</>
);
};
In this example, we have defined a reducer function that takes the current state and an action as arguments, and returns a new state based on the action type. We have also defined an initial state 0.
We then use the useReducer hook to create a state and a dispatch function that we can use to update the state. We pass our reducer function and initial state to useReducer, and it returns the current state and the dispatch function.
We use the dispatch function to send actions to our reducer function whenever the user clicks the + or - buttons. The reducer function then updates the state based on the action, and the component re-renders with the new state. Simple!
It is understandable to wonder about the advantages of using useReducer
over useState
. In situations where the complexity of logic is uncertain, it may be initially simpler to utilize a few instances of useState
. However, as the logic and states expand, the codebase can become increasingly complex and difficult to read. In these scenarios, utilizing useReducer
can prove to be a more efficient and effective solution for managing state and logic within the application.
When to choose useReducer?
So, when is it appropriate to use useState
and when should you consider using useReducer
? Here are some general guidelines to help you make your decision:
If you only have a single piece of state and the logic for updating it is simple, useState is probably the way to go.
If you have multiple pieces of state that are related and their updates depend on one another, useReducer might be a better fit.
If you have state that needs to be updated based on an action or event (such as a form submission or a button click), useReducer is a good choice because it allows you to define a reducer function that handles the various actions.
useReducer
is similar to the useState
hook in that it allows you to manage the state of a component, but it provides an additional layer of abstraction by allowing you to move the state update logic into a single, separate function outside of your component. This can make your code more readable and easier to reason about, particularly in situations where the state logic becomes complex. It's important to note that while useReducer
can provide more powerful and flexible state management, it also requires a deeper understanding of how it works.
The combination of useReducer
and useContext
is an effective approach when working with global states across multiple components in a React application. The useContext
hook allows for easy access to the global state, while useReducer
provides a clear and organized method for managing and updating that state. It is worth noting that the use of the Context API is a topic that warrants further discussion and I will be happy to provide additional insights on the matter in a future post (part-2). Now, let's move on to exploring a demonstration of the use of useReducer in a slightly more complex state logic, one that involves more than just a primitive value as we have previously used.
Here is an example of using the useReducer
hook to handle a form with multiple inputs.
reducer.js :
export const initialState = {
values: {
name: '',
email: '',
password: '',
confirmPassword: '',
},
errors: {
name: '',
email: '',
password: '',
confirmPassword: '',
},
isSubmitting: false,
};
//reducer function. This is where we will have our state logic.
export const formReducer = (state, action) => {
switch (action.type) {
case 'updateValue':
return {
...state,
values: {
...state.values,
[action.field]: action.value,
},
};
case 'validate':
const errors = {};
if (!state.values.name) {
errors.name = 'Name is required';
}
if (!state.values.email) {
errors.email = 'Email is required';
}
if (!state.values.password) {
errors.password = 'Password is required';
}
return {
...state,
errors,
};
case 'submit':
return {
...state,
isSubmitting: true,
};
case 'success':
return initialState;
case 'error':
return {
...state,
isSubmitting: false,
};
default:
return state;
}
};
The initialState
variable defines the initial values for the form inputs, as well as the errors and the submitting status of the form.
The formReducer
function takes in the current state and an action, and returns the new state based on the action type. The actions that can be dispatched to the reducer are:
updateValue
: updates the value of a specific field in the formvalidate
: performs validation checks on all the fields in the form and updates the error state accordinglysubmit
: sets the form's submitting status to truesuccess
: resets the form to its initial stateerror
: sets the form's submitting status to false
Form.jsx :
import { useReducer } from 'react';
import { formReducer, initialState } from 'reducer';
const Form = () => {
const [state, dispatch] = useReducer(formReducer, initialState);
const handleChange = (event) => {
dispatch({
type: 'updateValue',
field: event.target.name,
value: event.target.value,
});
};
const handleSubmit = (event) => {
event.preventDefault();
dispatch({ type: 'validate' });
if (!Object.values(state.errors).some((error) => error)) {
dispatch({ type: 'submit' });
// Make API call to submit form
// On success: dispatch({ type: 'success' });
// On error: dispatch({ type: 'error' });
}
}
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="name">Name:</label>
<input
type="text"
id="name"
name="name"
value={state.values.name}
onChange={handleChange}
/>
{state.errors.name && <p>{state.errors.name}</p>}
</div>
<div>
<label htmlFor="email">Email:</label>
<input
type="email"
id="email"
name="email"
value={state.values.email}
onChange={handleChange}
/>
{state.errors.email && <p>{state.errors.email}</p>}
</div>
<div>
<label htmlFor="password">Password:</label>
<input
type="password"
id="password"
name="password"
value={state.values.password}
onChange={handleChange}
/>
{state.errors.password && <p>{state.errors.password}</p>}
</div>
<div>
<label htmlFor="confirmPassword">Confirm Password:</label>
<input
type="password"
id="confirmPassword"
name="confirmPassword"
value={state.values.confirmPassword}
onChange={handleChange}
/>
{state.errors.confirmPassword && <p>{state.errors.confirmPassword}</p>}
</div>
<button type="submit" disabled={state.isSubmitting}>
Submit
</button>
</form>
);
}
The Form
component uses the useReducer
hook to create a state and a dispatch function from the formReducer
and the initialState
. The component has a number of input fields that are bound to the state values and have their onChange
event handlers set to the handleChange
function, which dispatches the updateValue
action to the reducer. The form also has a onSubmit
event handler set to the handleSubmit
function, which dispatches the validate
action to the reducer, and then if there are no errors it will dispatch the submit
action. After that, it uses the form data to make an API call and on success it dispatches the success
action and on error it dispatches the error
action.
Summary
The centralization of state logic within the reducer function allows for improved readability and maintainability of the codebase. As the development process progresses, new cases can easily be added to the reducer function without the need to import multiple instances of useState
, thereby avoiding an unmanageable codebase.
Overall, the choice between useState
and useReducer
comes down to the complexity of your state and how you need to update it. Both useState and useReducer are useful tools in the React developer's toolkit, and learning when to use each one will help you build efficient and maintainable applications.
Conclusion
In conclusion, this article has provided an overview of the use of useReducer in React, including a comparison of its functionality and benefits in relation to the traditional useState hook. We have also discussed the concept of a reducer and its connection to useReducer. This is just the beginning of the journey, in the next part of this series we will dive deeper into the usage and implementation of useReducer in React Applications. We will examine the advantages of combining useReducer and useContext in managing state and how it differs from useState. Stay tuned!