Mastering React's useReducer: From Basics to Context API Integration

State management is a cornerstone of building robust React applications. While useState
is perfect for simple cases, as your app grows, you might need a more structured approach. Enter useReducer
—a hook that brings the power of reducers (like in Redux) to your functional components, and when combined with the Context API, unlocks scalable global state management.
1. What is useReducer and When Should You Use It?
useReducer
is a React hook for managing state logic that’s more complex than what useState
can handle. It’s especially useful when:
Your state logic involves multiple sub-values.
The next state depends on the previous one.
You want to centralize state updates for clarity and maintainability.
How Does useReducer Work?
Reducer function: Determines how state changes in response to actions.
Initial state: The starting value for your state.
Dispatch function: Triggers state changes by sending actions to the reducer.
“A typical reducer function receives the current state and an action, and returns a new state based on the action type.”
2. Basic useReducer Example: Building a Counter
Let’s start with a classic example—a counter.
import React, { useReducer } from 'react';
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
</div>
);
}
What’s happening here?
useReducer
manages the state object{ count: 0 }
.The reducer function handles
increment
anddecrement
actions.dispatch
is called when buttons are clicked to update the state.
3. A More Practical Example: Todo List with useReducer
Let’s look at a more real-world scenario—a todo list
const initialState = { todoList: [] };
function reducer(state, action) {
switch (action.type) {
case 'ADD_TODO':
return { ...state, todoList: [...state.todoList, action.payload] };
case 'REMOVE_TODO':
return { ...state, todoList: state.todoList.filter((_, i) => i !== action.payload) };
default:
return state;
}
}
You can now use this reducer in a component to add and remove todos. This pattern keeps state logic centralized and predictable, especially as your app grows.
4. Why Combine useReducer with Context API?
As your app scales, you’ll want to share state across many components. Passing props down multiple levels (prop drilling) gets messy. The Context API solves this by making state available anywhere in the component tree.
Combining Context API with useReducer gives you:
Centralized, predictable state logic (via reducer).
Global access to state and dispatch (via context).
A lightweight alternative to Redux.
5. Step-by-Step: useReducer + Context API
a. Define Initial State and Reducer
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + 1 };
case 'decrement':
return { ...state, count: state.count - 1 };
default:
throw new Error(`Unknown action: ${action.type}`);
}
}
b.create context
import React, { createContext, useReducer, useContext } from 'react';
const CountContext = createContext();
c.provider component
function CountProvider({ children }) {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<CountContext.Provider value={{ state, dispatch }}>
{children}
</CountContext.Provider>
);
}
Wrap your app (or a part of it) with this provider.
d. Using Context in Components
function Counter() {
const { state, dispatch } = useContext(CountContext);
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
</div>
);
}
Now, any component inside CountProvider
can access and update the count—no prop drilling required!
6. Real-World Example: Global Todo List
Let’s combine everything in a global todo app:
// 1. Define initial state and reducer
const initialState = { todos: [] };
function reducer(state, action) {
switch (action.type) {
case 'ADD_TODO':
return { ...state, todos: [...state.todos, action.payload] };
case 'REMOVE_TODO':
return { ...state, todos: state.todos.filter((_, i) => i !== action.payload) };
default:
return state;
}
}
// 2. Create context and provider
const TodoContext = React.createContext();
function TodoProvider({ children }) {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<TodoContext.Provider value={{ state, dispatch }}>
{children}
</TodoContext.Provider>
);
}
// 3. Use context in a component
function TodoList() {
const { state, dispatch } = useContext(TodoContext);
const [input, setInput] = React.useState('');
return (
<div>
<input value={input} onChange={e => setInput(e.target.value)} />
<button onClick={() => {
dispatch({ type: 'ADD_TODO', payload: input });
setInput('');
}}>Add Todo</button>
<ul>
{state.todos.map((todo, i) => (
<li key={i}>
{todo}
<button onClick={() => dispatch({ type: 'REMOVE_TODO', payload: i })}>Remove</button>
</li>
))}
</ul>
</div>
);
}
// 4. Wrap your app
function App() {
return (
<TodoProvider>
<TodoList />
</TodoProvider>
);
}
7. Conclusion
useReducer is great for complex or interrelated state.
Context API lets you share state globally.
Combining them gives you scalable, maintainable state management—no Redux or extra libraries needed.
Subscribe to my newsletter
Read articles from deashot directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
