How to fix useReducer received an invalid reducer function in React
The "useReducer received an invalid reducer function" error occurs when React's useReducer hook receives a reducer function that doesn't meet the required criteria. This typically happens when the reducer mutates state directly, doesn't return a value for all action types, or has an incorrect function signature.
Quick fix
Fixes useReducer received an invalid reducer function
// ❌ WRONG - mutating state directly
function reducer(state, action) {
switch (action.type) {
case 'increment':
state.count = state.count + 1; // Mutation!
return state;
default:
return state;
}
}
// ✅ CORRECT - returning new state
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {
...state,
count: state.count + 1
};
default:
return state;
}
}- Ensure reducer is a pure function that doesn't mutate state
- Add a default case that returns the current state
- Verify reducer function signature and return values
- Remove side effects from the reducer
// ❌ WRONG - mutating state directlyfunction reducer(state, action) {switch (action.type) {case 'increment':state.count = state.count + 1; // Mutation!return state;default:return state;}}// ✅ CORRECT - returning new statefunction reducer(state, action) {switch (action.type) {case 'increment':return {...state,count: state.count + 1};default:return state;}}useReducer received an invalid reducer function
What this error means
This error occurs when React's useReducer hook receives a reducer function that violates one or more of React's requirements for reducer functions. Reducers in React must be pure functions that take the current state and an action, then return the next state without side effects. When React detects that a reducer function doesn't meet these criteria, it throws this error to prevent unpredictable behavior. The error is a safeguard against common mistakes that can lead to bugs that are difficult to debug, such as state mutations that don't trigger re-renders or inconsistent state updates. Understanding this error is crucial for working with complex state logic in React applications, as useReducer is often used to manage state that involves multiple sub-values or when the next state depends on the previous one in non-trivial ways.
Typical symptoms
- Error message states "useReducer received an invalid reducer function"
- Application crashes when trying to dispatch an action to the reducer
- State becomes undefined or null unexpectedly
- Components don't re-render when actions are dispatched
- State updates appear to be ignored or don't persist
- Error occurs after refactoring or modifying reducer logic
- Reducer works in some cases but fails for specific action types
Common causes
- Reducer function mutates the state object directly instead of returning a new state
- Missing return statement for some action types in the reducer
- Reducer function doesn't handle all possible action types (no default case)
- Incorrect function signature - reducer doesn't accept (state, action) parameters
- Reducer returns undefined for some action types
- Using side effects (API calls, alerts, etc.) inside the reducer function
- Reducer function is not a pure function (depends on external variables)
- Passing a regular function instead of a reducer function to useReducer
How to fix "useReducer received an invalid reducer function"
Check that your reducer always returns a new state object instead of mutating the existing one:
// ❌ WRONG - mutating state directly
function reducer(state, action) {
switch (action.type) {
case 'increment':
state.count = state.count + 1; // Mutation!
return state;
default:
return state;
}
}
// ✅ CORRECT - returning new state
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {
...state,
count: state.count + 1
};
default:
return state;
}
}For arrays, use methods that return new arrays:
// ❌ WRONG - mutating array
case 'add_item':
state.items.push(action.item); // Mutation!
return state;
// ✅ CORRECT - returning new array
case 'add_item':
return {
...state,
items: [...state.items, action.item]
};Ensure your reducer handles all possible action types, including unknown ones:
// ❌ WRONG - no default case
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
// Missing default case!
}
}
// ✅ CORRECT - with default case
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
return state; // Always return current state for unknown actions
}
}Alternatively, you can throw an error for unknown actions:
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(`Unknown action type: ${action.type}`);
}
}Check that your reducer accepts exactly two parameters (state, action) and always returns a value:
// ❌ WRONG - incorrect signature
function reducer() { // Missing parameters
return { count: 0 };
}
// ❌ WRONG - missing return
function reducer(state, action) {
switch (action.type) {
case 'increment':
const newCount = state.count + 1;
// No return statement!
}
}
// ✅ CORRECT - proper signature and returns
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
return state;
}
}Also ensure each case block has its own scope with curly braces:
// ❌ RISKY - variables might clash between cases
case 'add_item':
const newItem = action.item;
return { ...state, items: [...state.items, newItem] };
case 'remove_item':
const itemId = action.id; // Same scope as newItem!
return { ...state, items: state.items.filter(item => item.id !== itemId) };
// ✅ CORRECT - each case has its own scope
case 'add_item': {
const newItem = action.item;
return { ...state, items: [...state.items, newItem] };
}
case 'remove_item': {
const itemId = action.id;
return { ...state, items: state.items.filter(item => item.id !== itemId) };
}Ensure your reducer doesn't contain any side effects like API calls, alerts, or timeouts:
// ❌ WRONG - side effects in reducer
function reducer(state, action) {
switch (action.type) {
case 'fetch_data':
fetch('/api/data') // Side effect!
.then(response => response.json())
.then(data => {
// Can't return from here
});
return state;
case 'show_message':
alert(action.message); // Side effect!
return { ...state, message: action.message };
}
}
// ✅ CORRECT - side effects in event handlers or useEffect
function reducer(state, action) {
switch (action.type) {
case 'set_data':
return { ...state, data: action.data };
case 'set_message':
return { ...state, message: action.message };
default:
return state;
}
}
// Handle side effects elsewhere
function MyComponent() {
const [state, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
fetch('/api/data')
.then(response => response.json())
.then(data => {
dispatch({ type: 'set_data', data });
});
}, []);
const handleClick = () => {
alert(state.message);
};
return <button onClick={handleClick}>Show Message</button>;
}Write simple tests to verify your reducer works correctly:
// reducer.test.js
import { reducer } from './reducer';
describe('reducer', () => {
const initialState = { count: 0 };
test('handles increment action', () => {
const newState = reducer(initialState, { type: 'increment' });
expect(newState).toEqual({ count: 1 });
expect(newState).not.toBe(initialState); // Should be new object
});
test('handles decrement action', () => {
const newState = reducer(initialState, { type: 'decrement' });
expect(newState).toEqual({ count: -1 });
});
test('returns current state for unknown action', () => {
const newState = reducer(initialState, { type: 'unknown' });
expect(newState).toBe(initialState); // Should return same reference
});
test('does not mutate original state', () => {
const originalState = { count: 0 };
const newState = reducer(originalState, { type: 'increment' });
expect(originalState.count).toBe(0); // Original unchanged
expect(newState.count).toBe(1);
});
});Run these tests to ensure your reducer is pure and handles all cases correctly.
For complex state structures, consider using Immer to simplify reducer logic while maintaining purity:
npm install immerimport { produce } from 'immer';
function reducer(state, action) {
return produce(state, draftState => {
switch (action.type) {
case 'add_item':
draftState.items.push(action.item); // Safe mutation with Immer
break;
case 'update_user':
draftState.user.name = action.name;
draftState.user.email = action.email;
break;
case 'nested_update':
draftState.data.users[action.userId].profile.avatar = action.avatar;
break;
default:
break;
}
});
}Immer creates a "draft" state that you can mutate, then automatically produces an immutable update. This makes complex updates much more readable while keeping the reducer pure.
Ensure your action objects contain all required fields and have the correct structure:
// ❌ WRONG - missing required data
dispatch({ type: 'update_user' });
// Reducer expects action.name and action.email
// ✅ CORRECT - complete action object
dispatch({
type: 'update_user',
name: 'John',
email: 'john@example.com'
});
// Consider using action creator functions
function updateUser(name, email) {
return { type: 'update_user', name, email };
}
dispatch(updateUser('John', 'john@example.com'));Also ensure your reducer properly extracts data from actions:
function reducer(state, action) {
switch (action.type) {
case 'update_user':
// Always check for existence if fields might be missing
return {
...state,
user: {
...state.user,
name: action.name || state.user.name,
email: action.email || state.user.email
}
};
default:
return state;
}
}Advanced notes
When working with TypeScript, you can add strong typing to prevent many of these errors:
type Action =
| { type: 'increment'; amount: number }
| { type: 'decrement'; amount: number }
| { type: 'reset' };
type State = { count: number };
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'increment':
return { count: state.count + action.amount };
case 'decrement':
return { count: state.count - action.amount };
case 'reset':
return { count: 0 };
default:
// TypeScript ensures all cases are handled
const _exhaustiveCheck: never = action;
return _exhaustiveCheck;
}
}TypeScript will catch missing cases, incorrect action types, and missing properties at compile time.
For very large applications, consider splitting reducers using the "reducer composition" pattern or using libraries like Redux Toolkit which provides utilities like createSlice that automatically generate action creators and reducers with proper immutability.
Frequently asked questions
- What does "useReducer received an invalid reducer function" mean?
- This error occurs when React's useReducer hook receives a reducer function that violates one or more of React's requirements for reducer functions. Reducers in React must be pure functions that take the current state and an action, then return the next state without side effects. When React detects that a reducer function doesn't meet these criteria, it throws this error to prevent unpredictable behavior.
- How do I fix "useReducer received an invalid reducer function"?
- The "useReducer received an invalid reducer function" error occurs when React's useReducer hook receives a reducer function that doesn't meet the required criteria. This typically happens when the reducer mutates state directly, doesn't return a value for all action types, or has an incorrect function signature.
- What causes "useReducer received an invalid reducer function"?
- Reducer function mutates the state object directly instead of returning a new state Missing return statement for some action types in the reducer Reducer function doesn't handle all possible action types (no default case)
- Is the "useReducer received an invalid reducer function" error serious?
- It is a medium-severity error — more of a blocker than a risk. The steps above resolve it.
Official resources & further reading
- 📚React Official Documentation
- 📖React Official Docs: useReducer Hook(Official Docs)
- 📖React Official Docs: Extracting State Logic into a Reducer(Official Docs)
- 🐙