useReducer Simplified
Managing complex state logic in functional components can be challenging. While useState() is great for simple values, using it for objects with multiple sub-values often leads to messy code. The useReducer() hook is the ideal solution for these scenarios.
The useReducer Hook
The useReducer is a React hook designed to manage complex state logic through a central function that determines how data changes in response to specific actions. Its main advantage over useState is its ability to organize update patterns involving multiple sub-values or interdependent conditions, preventing the need for several isolated states.
By centralizing the business logic in one place, useReducer makes the code more predictable, easier to debug, and allows for a clean separation between data manipulation logic and the component's visual structure.
Step-by-Step Implementation
Following the logical flow, here is how you build a functional Shopping Cart using useReducer.
1) Define the initial state of the object
First, we define our component and establish the starting point for our data.
function ShoppingCart(){
const initialState = {
cart: [],
tQuantity: 0,
tPrice: 0,
};
2) Define a reducer function
Then we define a reducer funcion. This function takes two parameters: state (the current state) and action (which describes the operation to be performed).
The reducer actionhas two main parts: action.type, which describes the operation being executed ('ADD_ITEM','REMOVE_ITEM'), and action.payload, which carries the additional data necessary to perform the operation.
function reducer(state, action) {
switch (action.type) {
case 'ADD_ITEM':
return {
...state, // Spread operator to keep other state properties
cart: [...state.cart, action.payload],
tQuantity: state.tQuantity + 1,
tPrice: state.tPrice + action.payload.price,
};
case 'REMOVE_ITEM':
// We use the index to identify the specific item
const itemToRemove = state.cart[action.payload];
return {
...state,
// Filter creates a new array excluding the selected index
cart: state.cart.filter((_, index) => index !== action.payload),
tQuantity: state.tQuantity - 1,
tPrice: state.tPrice - itemToRemove.price,
};
default:
return state;
}
}
3) Deconstruct the hook into variables
Inside the ShoppingCart component, we initialize the hook. It returns the current state and a dispatch function, which is used to send actions to the reducer.
const [state, dispatch] = useReducer(reducer, initialState);
Recommended by LinkedIn
4) Create arrow functions for each action
Then, we create arrow functions to make using each reducer action more straightforward, passing the necessary payload for each one. The payload contains the essential data the reducer needs to perform the specific state update.
For 'ADD_ITEM', the payload is an object with the item details ( item and price). For 'REMOVE_ITEM', the payload is simply the index of the item to be removed.
const addItem = (item, price) => {
dispatch({ type: 'ADD_ITEM', payload: { item, price } });
};
const removeItem = (index) => {
dispatch({ type: 'REMOVE_ITEM', payload: index });
};
5) Finalize the component return (JSX)
Now, we render the interface, using the state to display data and the helper functions to handle user clicks.
return (
<div>
<h1>Shopping Cart</h1>
<button onClick={() => addItem('Item 1', 10)}>Add Item 1 ($10)</button>
<button onClick={() => addItem('Item 2', 15)}>Add Item 2 ($15)</button>
<ul>
{state.cart.map((entry, index) => (
<li key={index}>
{entry.item}
<button onClick={() => removeItem(index)}>Remove</button>
</li>
))}
</ul>
<p>Total Quantity: {state.tQuantity}</p>
<p>Total Price: ${state.tPrice}</p>
</div>
);
}
6) Code Best Practices
To keep the code clean, it is recommended to move the initialState and reducer function into a separate file (e.g., cartReducer.js) and import them:
import { initialState, reducer } from './cartReducer';
This allows the component to focus on rendering while the logic becomes reusable for other parts of the application.
Conclusion
The useReducer hook is a powerful tool for Local State Management.
It provides a structured way to handle complex data transitions within a single component or its direct children, offering much more control than useState as your application grows. By separating the "what happens" (actions) from the "how it happens" (reducer), your code becomes more professional and maintainable.
However, when you need to share state across the entire application (like user authentication or global settings), you should use Redux. Redux is a Global State Management tool that operates on the same concepts of a "Store," "Reducers," and "Actions," but on a global scale.
We will talk more about Redux in the next articles.