A reducer's signature is always (state, action) => newState, and it must be a pure function: given the same state and action inputs, it must always return the same new state output, with no side effects like API calls, random values, or mutations of the existing state object performed inside it. When an action doesn't match anything the reducer cares about, the convention is to return the existing state completely unchanged via a default case, rather than returning undefined or something else — the name reducer itself borrows from JavaScript's Array.prototype.reduce(), since Redux essentially reduces a whole sequence of dispatched actions down into a single, current state value over time.
1Understanding Reducers
A reducer's signature is always (state, action) => newState, and it must be a pure function: given the same state and action inputs, it must always return the same new state output, with no side effects like API calls, random values, or mutations of the existing state object performed inside it. When an action doesn't match anything the reducer cares about, the convention is to return the existing state completely unchanged via a default case, rather than returning undefined or something else — the name reducer itself borrows from JavaScript's Array.prototype.reduce(), since Redux essentially reduces a whole sequence of dispatched actions down into a single, current state value over time.
Never mutate the existing state object directly inside a reducer, like state.value = 5 or state.items.push(newItem) — always return a brand-new object/array with the needed changes, like { ...state, value: 5 }, since Redux and React rely on detecting state changes via reference comparison.
function counterReducer(state = { count: 0 }, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
return state;
}
}
console.log(counterReducer({ count: 5 }, { type: 'increment' }));2Practical Example
Here is a real-world application of Reducers showing how it is used in production React code.
function todosReducer(state = [], action) {
switch (action.type) {
case 'add_todo':
return [...state, { text: action.payload, done: false }];
default:
return state;
}
}
console.log(todosReducer([], { type: 'add_todo', payload: 'Buy milk' }));3Best Practices
Follow these guidelines when working with Reducers:
1. Keep reducers pure: no API calls, no random values, no direct mutation of the existing state, computing and returning a new state object based only on the given state and action
2. Always include a default case that returns the existing state unchanged, for any action type the reducer doesn't specifically handle
3. Use the spread operator, or a library like Immer via Redux Toolkit, to produce a new state object/array without mutating the original, rather than modifying nested properties in place
Tip: Never mutate the existing state object directly inside a reducer, like state.value = 5 or state.items.push(newItem) — always return a brand-new object/array with the needed changes, like { ...state, value: 5 }, since Redux and React rely on detecting state changes via reference comparison.
function counterReducer(state = { count: 0 }, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
return state;
}
}
console.log(counterReducer({ count: 5 }, { type: 'increment' }));