The useReducer hook is a powerful tool in React for managing complex state logic in components. It offers an alternative to useState when you need more control over state updates, especially when dealing with multiple state values or actions that manipulate your state in different ways.
When to Use useReducer
You should consider using useReducer over useState in the following scenarios:
-
Complex state logic: If your component state involves multiple values or updates that depend on each other,
useReducerhelps you centralize the logic. -
Multiple actions: When you need to handle several different actions that affect the state,
useReducerprovides a more structured approach. -
State dependent on previous state: If state updates are dependent on the current state (like toggling between values),
useReducerensures consistency in handling such updates.
How useReducer Works
useReducer works by taking in two things:
- Reducer function: This function determines how the state should change based on the action dispatched.
- Initial state: The initial state can be an object, array, or any data structure you need.
useReducer returns:
- State: The current state of the component.
- Dispatch: A function used to send actions to the reducer to update the state.
Setting Up useReducer
- Define the Initial State: This can be an object with multiple properties. For example:
const defaultState = {
people: data, // data is an array of people
};
-
Create the Reducer Function: The reducer function takes two parameters:
stateandaction. The action contains thetype(the action type) and any additional data needed. Based on the action type, the reducer updates the state.
const reducer = (state, action) => {
switch (action.type) {
case 'REMOVE_ITEM':
return {
...state,
people: state.people.filter(person => person.id !== action.id),
};
case 'RESET_ITEMS':
return {
...state,
people: data, // reset to initial data
};
case 'CLEAR_ITEMS':
return {
...state,
people: [], // clears the list
};
default:
return state;
}
};
-
Use the
useReducerHook: CalluseReducer, passing in the reducer function and initial state.
const [state, dispatch] = useReducer(reducer, defaultState);
-
Dispatching Actions: To modify the state, you dispatch an action. Each action should have a
typeand may include additional properties, such as anidto remove an item.
const removeItem = (id) => {
dispatch({ type: 'REMOVE_ITEM', id });
};
const clearItems = () => {
dispatch({ type: 'CLEAR_ITEMS' });
};
const resetItems = () => {
dispatch({ type: 'RESET_ITEMS' });
};
Example: A List of People
Let’s implement a list where users can remove items, clear the list, or reset it using useReducer.
import { useReducer } from 'react';
import { data } from '../../../data'; // initial data
const defaultState = {
people: data,
};
const reducer = (state, action) => {
switch (action.type) {
case 'REMOVE_ITEM':
return {
...state,
people: state.people.filter(person => person.id !== action.id),
};
case 'RESET_ITEMS':
return {
...state,
people: data, // reset to original data
};
case 'CLEAR_ITEMS':
return {
...state,
people: [], // clear all items
};
default:
return state;
}
};
const App = () => {
const [state, dispatch] = useReducer(reducer, defaultState);
const removeItem = (id) => {
dispatch({ type: 'REMOVE_ITEM', id });
};
const clearItems = () => {
dispatch({ type: 'CLEAR_ITEMS' });
};
const resetItems = () => {
dispatch({ type: 'RESET_ITEMS' });
};
return (
<div>
{state.people.map((person) => {
const { id, name } = person;
return (
<div key={id} className="item">
<h4>{name}</h4>
<button onClick={() => removeItem(id)}>remove</button>
</div>
);
})}
{state.people.length ? (
<button
className="btn"
style={{ marginTop: '2rem' }}
onClick={clearItems}
>
clear items
</button>
) : (
<button
className="btn"
style={{ marginTop: '2rem' }}
onClick={resetItems}
>
reset items
</button>
)}
</div>
);
};
export default App;
Key Points to Remember
- Reducer Function: This is a pure function that receives the current state and an action, and returns a new state.
-
Action Type: Each action must have a
typeto identify the action and determine what to do with the state. - Dispatch: The dispatch function sends actions to the reducer, which processes them and updates the state accordingly.
-
State Update: Unlike
useState, which directly updates the state,useReducerrequires dispatching an action that triggers state updates in the reducer function.
When Not to Use useReducer
-
Simple State Management: If your component’s state is simple (for example, a boolean flag or a single number),
useStateis easier and more straightforward. -
Low Complexity: If the state doesn’t need to change based on various actions,
useStatecan be simpler to implement.
Conclusion
useReducer is a great tool for handling complex state logic, especially when managing multiple pieces of state or when updates depend on the previous state. It gives you more control over your state and helps organize your component logic when the state management grows in complexity.
Top comments (3)
Action Naming Conventions in
useReducerIn React, when using
useReducer, it's a good practice to define action types as constants instead of writing them as strings directly. This helps prevent typos and makes your code more reliable.Why Use Constants?
If you use strings directly, you might accidentally mistype the action type, leading to errors. By using constants, you can avoid this issue.
For example, instead of:
You can define a constant:
Then, use the constant both when dispatching and checking the action type in the reducer:
And in the reducer:
This makes it easier to spot typos, as the constant will be consistent throughout the code. If an invalid action is dispatched, the error will be clear, helping you debug faster.
Conclusion:
Using constants for action types prevents typos and makes your code cleaner and easier to debug.
Passing Data to Actions with "Payload"
In
useReducer, when you pass data to an action, it's common to include it inside a property calledpayload. This is a convention that makes it easier to handle data, even if you only need to pass one value like anid. By usingpayload, you can ensure consistency and clarity in how data is handled in the reducer.Example:
When dispatching an action like
REMOVE_ITEM, you pass theidinside apayloadobject:In the reducer, you access the
idfromaction.payload.id:This helps keep the action object consistent and organized, especially as your application grows and you need to pass more complex data. Even if you're just passing a single value like
id, wrapping it inpayloadensures that future changes or additional data can be added without disrupting the structure.Conclusion:
Using
payloadto pass data in actions is a convention that helps keep the code clean and consistent. It makes it easy to scale and handle different types of data in actions, even if you're just passing a simple value like anid.Organizing Actions and Reducers in Separate Files
As your application grows and the number of actions increases, it's a good practice to split your actions and reducer into separate files to keep the code clean and manageable.
actions.js): Store all the action types as constants in a separate file. This way, you avoid hardcoding strings and make it easier to manage action types.Example:
reducer.js): In this file, handle the state changes based on the action types imported from the actions file. This keeps your reducer focused solely on state management.Example:
Example:
By following this structure, you ensure that your code remains organized and easier to maintain as it grows. This is especially useful when managing a large number of actions in your app.