How to Handle Complex State with useReducer in React
useReducer hook to manage complex state by defining a reducer function that handles state changes based on action types. This keeps state logic organized and predictable, especially when state has multiple related values or complex updates.Why This Happens
When you try to manage complex state with useState, you often end up writing many state variables and update functions. This makes your code hard to read and maintain. Also, updating related pieces of state separately can cause bugs or inconsistent UI.
import React, { useState } from 'react'; function Form() { const [name, setName] = useState(''); const [email, setEmail] = useState(''); const [age, setAge] = useState(''); function handleNameChange(e) { setName(e.target.value); } function handleEmailChange(e) { setEmail(e.target.value); } function handleAgeChange(e) { setAge(e.target.value); } return ( <form> <input value={name} onChange={handleNameChange} placeholder="Name" /> <input value={email} onChange={handleEmailChange} placeholder="Email" /> <input value={age} onChange={handleAgeChange} placeholder="Age" /> </form> ); }
The Fix
Replace multiple useState calls with a single useReducer hook. Define a reducer function that updates state based on action types. This centralizes state logic and makes updates predictable and easier to manage.
import React, { useReducer } from 'react'; const initialState = { name: '', email: '', age: '' }; function reducer(state, action) { switch (action.type) { case 'SET_NAME': return { ...state, name: action.payload }; case 'SET_EMAIL': return { ...state, email: action.payload }; case 'SET_AGE': return { ...state, age: action.payload }; default: return state; } } function Form() { const [state, dispatch] = useReducer(reducer, initialState); return ( <form> <input value={state.name} onChange={e => dispatch({ type: 'SET_NAME', payload: e.target.value })} placeholder="Name" /> <input value={state.email} onChange={e => dispatch({ type: 'SET_EMAIL', payload: e.target.value })} placeholder="Email" /> <input value={state.age} onChange={e => dispatch({ type: 'SET_AGE', payload: e.target.value })} placeholder="Age" /> </form> ); }
Prevention
Use useReducer from the start when your state has multiple related values or complex update logic. Keep your reducer function pure and simple by handling one action at a time. Use descriptive action types and keep state immutable by returning new objects. Tools like ESLint with React hooks rules help catch mistakes early.
Related Errors
Common mistakes include mutating state directly inside the reducer, which breaks React's state update rules and causes UI bugs. Another error is forgetting to handle default cases in the reducer, leading to unexpected state resets. Always return the current state by default.
function reducer(state, action) { if (action.type === 'SET_NAME') { // Wrong: mutating state directly // state.name = action.payload; // return state; // Correct approach: return { ...state, name: action.payload }; } return state; }