This React error occurs when a useReducer reducer function fails to return a proper state object, often due to missing return statements, unhandled action types, or returning undefined. The reducer must be a pure function that always returns a valid next state for every possible action. Understanding common patterns and debugging techniques helps prevent this state management issue.
React's useReducer hook expects the reducer function to be a pure function that takes the current state and an action, and returns the next state. This error occurs when the reducer fails to return a valid state object, typically by returning undefined, forgetting a return statement, or not handling all possible action types. When React calls the reducer during state updates, it expects a concrete state value to use for the next render. If the reducer returns undefined or nothing at all, React cannot proceed with the state update and throws this error. This is a critical issue because it breaks the fundamental contract of reducers in React's state management system.
Open your browser's developer tools (F12) and look for the error message. React usually includes a stack trace showing which component and reducer function is causing the issue. Note the line number and component name to locate the problematic reducer.
Ensure your reducer has a default case that either returns the current state or throws a descriptive error. This catches any unhandled action types during development.
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + 1 };
case 'decrement':
return { ...state, count: state.count - 1 };
default:
// Option 1: Return current state (silently ignore unknown actions)
// return state;
// Option 2: Throw error during development
throw new Error(`Unknown action type: ${action.type}`);
}
}Check each case in your switch statement (or each condition in if/else) to ensure it ends with a return statement. Common mistakes include using break instead of return, or forgetting to return after mutating state.
// WRONG - missing return after mutation
function badReducer(state, action) {
switch (action.type) {
case 'update':
state.value = action.payload; // Mutation!
break; // Should be return { ...state, value: action.payload };
case 'reset':
return initialState;
}
// No return here - ERROR!
}
// CORRECT - every case returns
function goodReducer(state, action) {
switch (action.type) {
case 'update':
return { ...state, value: action.payload };
case 'reset':
return initialState;
default:
return state;
}
}TypeScript can help identify branches that don't return a value. Configure your tsconfig.json with strict mode and enable noImplicitReturns to get warnings about functions that might not return a value.
{
"compilerOptions": {
"strict": true,
"noImplicitReturns": true
}
}TypeScript will then warn you:
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + 1 };
// TypeScript error: Not all code paths return a value
}
}If you're using if/else statements, consider converting to a switch statement which makes it clearer that each branch must return. Switch statements are generally preferred for reducers because they explicitly show all possible action types.
// Hard to track - if/else
function ifElseReducer(state, action) {
if (action.type === 'add') {
return { ...state, items: [...state.items, action.item] };
} else if (action.type === 'remove') {
return { ...state, items: state.items.filter(i => i.id !== action.id) };
} // Missing else clause!
}
// Clearer - switch statement
function switchReducer(state, action) {
switch (action.type) {
case 'add':
return { ...state, items: [...state.items, action.item] };
case 'remove':
return { ...state, items: state.items.filter(i => i.id !== action.id) };
default:
return state;
}
}Create unit tests for your reducer that dispatch each possible action type and verify the returned state. This helps catch missing returns and ensures all action types are handled.
import { describe, it, expect } from 'vitest';
import reducer from './reducer';
describe('reducer', () => {
const initialState = { count: 0 };
it('handles increment action', () => {
const result = reducer(initialState, { type: 'increment' });
expect(result).toEqual({ count: 1 });
});
it('handles decrement action', () => {
const result = reducer(initialState, { type: 'decrement' });
expect(result).toEqual({ count: -1 });
});
it('returns current state for unknown actions', () => {
const result = reducer(initialState, { type: 'unknown' });
expect(result).toBe(initialState);
});
});For complex reducers with many cases, break out logic into helper functions. This makes each case simpler and reduces the chance of forgetting a return statement.
function updateUser(state, action) {
return {
...state,
users: state.users.map(user =>
user.id === action.id ? { ...user, ...action.updates } : user
)
};
}
function addUser(state, action) {
return {
...state,
users: [...state.users, action.user]
};
}
function reducer(state, action) {
switch (action.type) {
case 'UPDATE_USER':
return updateUser(state, action);
case 'ADD_USER':
return addUser(state, action);
case 'DELETE_USER':
return deleteUser(state, action);
default:
return state;
}
}The useReducer hook is particularly sensitive to this error because reducers are meant to be pure functions with predictable behavior. In production builds, React might handle missing returns differently than in development. Some state management libraries like Redux Toolkit provide utilities (createSlice) that automatically generate reducers with proper return handling. For complex applications, consider using immer with useReducer to write mutable-looking code that automatically produces immutable updates. When debugging, remember that React Strict Mode double-invokes reducers in development, which can help catch impure reducer behavior early. If you're migrating from class component setState to useReducer, note that setState merges updates automatically while reducers must explicitly return complete state objects.
Prop spreading could cause security issues
Prop spreading could cause security issues
Error: error:0308010C:digital envelope routines::unsupported
Error: error:0308010C:digital envelope routines::unsupported
React Hook "useEffect" is called conditionally. React Hooks must be called in the exact same order in every component render.
React Hook useEffect placed inside a condition
Hook can only be called inside the body of a function component
Hook can only be called inside the body of a function component
Rollup failed to resolve import during build
How to fix "Rollup failed to resolve import" in React