Master useReducer to manage complex state transitions in React. Learn how to write a reducer function, dispatch actions, and clean up your component logic.
Previously in this course, we explored how to build custom hooks in Introduction to Custom Hooks to encapsulate logic. While custom hooks are excellent for sharing behavior, they don't always solve the problem of managing "spaghetti" state—where multiple useState calls lead to interdependent updates that are hard to track.
When your dashboard's state logic grows beyond simple toggles, useReducer becomes your most powerful tool. It allows you to move state transition logic out of your components and into a predictable, testable function.
In smaller components, useState is perfectly fine. However, as our dashboard project grows, we often face scenarios where:
setLoading(true), setError(null), setData(null)).useReducer solves this by forcing you to define all possible state transitions in one place: a reducer function. This pattern is foundational for React state management: Reducers vs. State Machines and keeps your components focused on rendering rather than logic orchestration.
To use useReducer, you need three things:
currentState and an action, then returns the nextState.Let's refactor our dashboard's data fetching logic. Instead of managing isLoading, data, and error as three separate useState variables, we'll group them into one object.
JAVASCRIPTimport { useReducer } from CE9178">'react'; // 1. Define the initial state const initialState = { data: null, loading: false, error: null, }; // 2. The reducer function: Pure logic for transitions function dashboardReducer(state, action) { switch (action.type) { case CE9178">'FETCH_START': return { ...state, loading: true, error: null }; case CE9178">'FETCH_SUCCESS': return { ...state, loading: false, data: action.payload }; case CE9178">'FETCH_ERROR': return { ...state, loading: false, error: action.payload }; default: return state; } } function Dashboard() { const [state, dispatch] = useReducer(dashboardReducer, initialState); const fetchData = async () => { dispatch({ type: CE9178">'FETCH_START' }); try { const response = await fetch(CE9178">'/api/dashboard'); const data = await response.json(); dispatch({ type: CE9178">'FETCH_SUCCESS', payload: data }); } catch (err) { dispatch({ type: CE9178">'FETCH_ERROR', payload: err.message }); } }; return ( <div> {state.loading && <p>Loading...</p>} {state.error && <p>Error: {state.error}</p>} {state.data && <pre>{JSON.stringify(state.data, null, 2)}</pre>} <button onClick={fetchData}>Refresh Data</button> </div> ); }
Notice how dispatch acts as an event emitter. The component no longer cares how the state is updated; it only broadcasts what happened. This separation is the key to maintaining a clean Review of State Management: Choosing the Right React Strategy as your app scales.
In your dashboard project, locate the component handling the user's "filter" settings (e.g., date range, category selection, and search term).
initialState object with these three fields.filterReducer that handles updates for each field.useState hooks with useReducer.SET_SEARCH_TERM doesn't accidentally wipe out your dateRange state.state object directly in your reducer. Always return a new object (e.g., using the spread operator { ...state }).useEffect.useReducer for simple counters or toggles. It adds boilerplate. If you only have one piece of state, useState is almost always the correct choice.By centralizing your logic, useReducer makes complex state transitions predictable. You define actions to describe intent and a reducer to handle the state change. This pattern prevents "impossible states"—like having both loading and error be true—because you control the transition logic explicitly.
Up next: We will dive deeper into managing object-based state, focusing on how to handle deeply nested data structures immutably.
Master cleanup functions in useEffect to handle component unmounting and cancel pending API requests, ensuring your React app stays stable and bug-free.
Read moreLearn to master loading state in React to improve UX. Discover how to conditionally render spinners while your API fetches data for your movie app.
Complex State with useReducer
Structuring State for Performance
Handling Authentication State
Integrating Reducers with Auth State
Introduction to React Router
Dynamic Routing with URL Parameters
Nested Routes and Layouts
Protected Routes for Authenticated Views
Programmatic Navigation
Building the Dashboard Navigation Structure
Asynchronous Data Lifecycle
Caching Strategies with React Query
Mutations and Data Updates
Synchronizing Client and Server State
Integrating Live Data into the Dashboard
Error Handling and Loading UI
Controlled vs Uncontrolled Components
Real-time Form Validation
Schema-based Validation with Zod
Handling Multi-step Forms
Optimizing Form Submissions
Performance Profiling with React DevTools
Refactoring for Scalability
Finalizing Dashboard Data Flow
Deploying the Application
Advanced Hook Composition
Implementing Middleware for State
Advanced Context Patterns
Router Loaders and Data Prefetching
Complex Route Guards
Handling Large Datasets in UI
Testing Hooks and Components
Managing Global Modals
Implementing Keyboard Shortcuts
Optimizing Asset Loading
Internationalization Basics
Managing WebSocket Connections