Welcome to the tutorial on Redux Reducers. Think of reducers as the chefs in a kitchen. While actions are the customer orders, reducers take those orders and decide what dish to prepare. They handle the logic of transforming the current state into the next state, based on the action they receive.
A reducer is a function that receives the current state and an action, determines how to update the state based on the action type, and then returns the next state. Reducers are pure functions and must avoid side effects or state mutation.
Analogy: Imagine the state as your kitchen inventory. When an order (action) comes in, the chef (reducer) checks what needs to change and returns a new updated inventory. But the chef never scribbles over the old inventory list; instead, they make a fresh copy with updates.
Here’s a basic example of a reducer that handles fruits in inventory:
const fruitReducer = (state = [], action) => {
switch (action.type) {
case 'ADD_FRUIT':
return [...state, action.fruit];
default:
return state;
}
};
When the Redux store initializes, it calls the reducer with an undefined state. The reducer sets its initial state to an empty array via the default parameter state = [].
If an action with type: 'ADD_FRUIT' is received, the reducer returns a new array with the new fruit appended. Otherwise, it returns the original state unchanged.
Let’s add more functionality. We can handle:
ADD_FRUITS - Add multiple fruitsSELL_FRUIT - Remove the first instance of a fruitSELL_OUT - Clear all fruitsconst fruitReducer = (state = [], action) => {
switch (action.type) {
case 'ADD_FRUIT':
return [...state, action.fruit];
case 'ADD_FRUITS':
return [...state, ...action.fruits];
case 'SELL_FRUIT': {
const index = state.indexOf(action.fruit);
if (index !== -1) {
const newState = [...state];
newState.splice(index, 1);
return newState;
}
return state;
}
case 'SELL_OUT':
return [];
default:
return state;
}
};
Note: We use [...state] to create a copy of the array. This is crucial because splice() modifies the array in place. By copying first, we avoid mutating the original state.
Reducers must never mutate their arguments. Always return a new object or array. Mutating the state can lead to unpredictable behavior and bugs.
Bad Reducer (mutates state):
const badReducer = (state = { count: 0 }, action) => {
switch (action.type) {
case 'INCREMENT_COUNTER':
state.count++;
return state;
default:
return state;
}
};
Good Reducer (copies state):
const goodReducer = (state = { count: 0 }, action) => {
switch (action.type) {
case 'INCREMENT_COUNTER': {
const nextState = Object.assign({}, state);
nextState.count++;
return nextState;
}
default:
return state;
}
};
As your app grows, managing everything in one reducer becomes messy. Just like hiring more chefs for different kitchen stations, split your reducer to manage slices of the state.
Example State:
{
fruit: ['APPLE', 'ORANGE'],
farmers: {
1: { id: 1, name: 'John Smith', paid: false },
2: { id: 2, name: 'Sally Jones', paid: false }
}
}
We need two reducers:
Redux’s combineReducers helps you stitch multiple reducers into one root reducer.
import { combineReducers, createStore } from 'redux';
const rootReducer = combineReducers({
fruit: fruitReducer,
farmers: farmersReducer
});
const store = createStore(rootReducer);
export default store;
Analogy: Think of combineReducers as assembling different departments in a company (fruit and farmers) under one management system (store).
fruitReducer.js and farmersReducer.js in src/reducers.store.js file in src/store and use combineReducers and createStore.In an e-commerce app, you may have:
cartReducer - Handles shopping cart itemsuserReducer - Manages user authenticationorderReducer - Processes order historyReducers are the core logic handlers of Redux. You learned how to:
Learn more at the official Redux documentation: Redux Documentation