useReducer for Complex State in React

While useState is great for handling simple state updates, managing more complex state—especially when the state depends on multiple variables or needs specific actions—can get messy. This is where useReducer comes in. It provides a more structured way to manage state updates in React, especially for large applications.

In this module, we’ll dive into how to use useReducer for managing complex state, when it’s appropriate to use over useState, and how to build an application with useReducer.


Table of Contents

  1. What is useReducer?
  2. When to Use useReducer
  3. Basic Syntax of useReducer
  4. Setting Up a Reducer Function
  5. Using useReducer for Complex State
  6. Combining Multiple Reducers
  7. Handling Actions with Payloads
  8. Using useReducer with Context API
  9. Code Example: Counter with useReducer
  10. Best Practices for Using useReducer
  11. Conclusion

1. What is useReducer?

useReducer is a hook in React that is used to manage more complex state logic in your components. It’s an alternative to useState, providing more flexibility and control, especially when you need to manage state that is an object, array, or needs a series of different actions to modify it.

At its core, useReducer lets you define a reducer function that determines how state should change in response to specific actions. This is similar to how Redux works, but at a component level.


2. When to Use useReducer

You should consider using useReducer when:

  • The state logic is complex (e.g., the next state depends on the previous state, or you need to perform multiple updates in one go).
  • You have a large state object that needs to be updated based on specific actions.
  • You need to handle multiple state transitions triggered by different user interactions or events.
  • You need performance optimizations by avoiding unnecessary re-renders, especially in large applications.

For simpler state, useState is sufficient, but useReducer shines when state management becomes more intricate.


3. Basic Syntax of useReducer

The useReducer hook accepts two arguments:

  1. Reducer Function: A function that describes how the state should change in response to specific actions.
  2. Initial State: The state that will be used when the component first mounts.

The useReducer hook returns an array, where the first element is the current state, and the second element is the dispatch function that you can use to trigger actions.

const [state, dispatch] = useReducer(reducer, initialState);

4. Setting Up a Reducer Function

A reducer function is a pure function that takes the current state and an action, and returns a new state. The structure of a reducer looks like this:

function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
return state;
}
}
  • state: The current state.
  • action: An object containing the action type and any necessary data (payload).

5. Using useReducer for Complex State

Here’s an example of how to use useReducer for a more complex state scenario, like managing a form’s state.

import { useReducer } from 'react';

const initialState = { name: '', email: '' };

function formReducer(state, action) {
switch (action.type) {
case 'set_name':
return { ...state, name: action.payload };
case 'set_email':
return { ...state, email: action.payload };
default:
return state;
}
}

function Form() {
const [state, dispatch] = useReducer(formReducer, initialState);

return (
<form>
<input
type="text"
value={state.name}
onChange={(e) => dispatch({ type: 'set_name', payload: e.target.value })}
/>
<input
type="email"
value={state.email}
onChange={(e) => dispatch({ type: 'set_email', payload: e.target.value })}
/>
</form>
);
}

In this example, useReducer is managing a form state, and the reducer handles updating the state based on specific actions (set_name, set_email).


6. Combining Multiple Reducers

If your application’s state is complex, you may have multiple independent pieces of state to manage. You can combine multiple reducers into a single one by creating a root reducer.

import { useReducer } from 'react';

const initialState = {
count: 0,
user: { name: '' }
};

function countReducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
default:
return state;
}
}

function userReducer(state, action) {
switch (action.type) {
case 'set_name':
return { user: { name: action.payload } };
default:
return state;
}
}

function rootReducer(state, action) {
return {
count: countReducer(state.count, action),
user: userReducer(state.user, action)
};
}

function App() {
const [state, dispatch] = useReducer(rootReducer, initialState);

return (
<div>
<button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
<input
type="text"
onChange={(e) => dispatch({ type: 'set_name', payload: e.target.value })}
/>
</div>
);
}

In this example, countReducer and userReducer are combined into one root reducer that updates two independent slices of state.


7. Handling Actions with Payloads

Many times, actions will need to carry additional data. You can pass this data in the payload of the action object. For instance, in a form, the data to be set in the state will often come from user input.

dispatch({ type: 'set_name', payload: 'John Doe' });

In the reducer, you can then use action.payload to update the state accordingly.


8. Using useReducer with Context API

For more complex or global state management, you can combine useReducer with the React Context API. This allows you to manage state at a higher level and pass the state and dispatch function down the component tree.

Example:

const StateContext = React.createContext();

function rootReducer(state, action) {
// Reducer logic
}

function StateProvider({ children }) {
const [state, dispatch] = useReducer(rootReducer, initialState);
return (
<StateContext.Provider value={{ state, dispatch }}>
{children}
</StateContext.Provider>
);
}

This setup allows you to share and modify state across components without prop drilling.


9. Code Example: Counter with useReducer

import { useReducer } from 'react';

const initialState = { count: 0 };

function counterReducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
return state;
}
}

function Counter() {
const [state, dispatch] = useReducer(counterReducer, initialState);

return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
<button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
</div>
);
}

10. Best Practices for Using useReducer

  • Start with useState: Use useReducer only when you need to manage complex state logic. For simple state, useState is sufficient.
  • Keep Reducers Pure: A reducer must be a pure function, meaning it should only return the next state and not have side effects.
  • Avoid Complex Logic in Reducers: While reducers can get complicated, try to keep them straightforward for easier debugging.
  • Combine Reducers: For large applications, break down the state into multiple slices and combine them into a root reducer for easier management.

11. Conclusion

useReducer is an essential hook in React for managing complex state logic. It provides a clear structure for state updates and improves scalability and maintainability, especially in larger applications. By using it with the Context API, you can efficiently manage global state without relying on prop drilling.