Writing the Task Reducer with combineReducers
Write the tasksReducer to handle CREATE_TASK and DELETE_TASK actions, then combine it with combineReducers so the state is accessible as state.tasks in every component.
The action types and creators are ready. Now write the function that handles them: the reducer. If you are not familiar with how reducers work, the pure Redux reducer post covers the foundation.
Two cases. One state shape. One barrel file with combineReducers.
The Tasks Reducer
Create src/reducers/tasks-reducer.js:
import { initialTasks } from '../data/tasks';
import * as actionTypes from '../constants/action-types';
export function tasksReducer(state = initialTasks, action) {
switch (action.type) {
case actionTypes.CREATE_TASK:
return [...state, action.payload];
case actionTypes.DELETE_TASK:
return state.filter((task) => task.id !== action.payload);
default:
return state;
}
}The function signature is (state = initialTasks, action). The default value initialTasks means the first time Redux calls this reducer, state starts as those three sample tasks. Every subsequent call receives the current state as it is.
CREATE_TASK: returns a new array containing every existing task plus action.payload, which is the new task object the action creator passed in. Spread (...state) copies the current tasks; action.payload adds the new one. The original array is never mutated.
DELETE_TASK: returns a filtered copy of state. Each task is kept only if its id does not match action.payload, the task id the component passed to deleteTask(). Again, no mutation.
default: returns state unchanged. Every unrecognized action type falls here. This is not a failure. Redux dispatches its own internal actions on startup, and they must fall through cleanly.
combineReducers
Real applications have multiple entities: tasks, users, notifications. Each gets its own reducer file. Redux provides combineReducers to merge them into one root reducer before passing to the store.
Create src/reducers/index.js:
import { combineReducers } from 'redux';
import { tasksReducer } from './tasks-reducer';
const allReducers = combineReducers({
tasks: tasksReducer,
});
export default allReducers;The object key (tasks) becomes the state slice name. Everywhere in the application you access task data as state.tasks, not state.tasksReducer. The key is user-defined. The function name is irrelevant once it is registered here.
Scalability: when you add customers or products, each gets a new entry:
const allReducers = combineReducers({
tasks: tasksReducer,
customers: customersReducer,
products: productsReducer,
});Each reducer owns its slice. state.tasks is managed only by tasksReducer. state.customers is managed only by customersReducer. They never interfere.
reducers/index.js as the entry point: like actions/index.js, this file is the barrel for the reducers folder. Importing the reducers folder from outside automatically resolves to this index.js. The store file imports allReducers in one line without knowing how many reducer files exist.
ExpandTask reducer flow: action dispatched → reducer → new state slice
The Essentials
- Reducer signature:
(state = initialTasks, action). The default state runs exactly once, on the store's first initialization. Every subsequent call receives the real current state. - Immutable updates:
CREATE_TASKuses spread to return a new array.DELETE_TASKusesfilter. Neither modifies the existing state object. Redux detects changes by reference. Mutation makes state appear unchanged. combineReducersnames the state slice. The key you pass (tasks) becomes the property name on the global state. Access task data asstate.taskseverywhere in the app.
Further Reading and Watching
- Redux combineReducers docs: official API reference
- Array.prototype.filter: MDN reference used in the DELETE_TASK case
reducers/tasks.json GitHub: the final reducer file with all async action type cases added in later chaptersreducers/index.json GitHub: the combineReducers barrel file
Keep reading