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.

June 27, 20263 min read4 / 5

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:

JavaScript
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:

JavaScript
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:

JavaScript
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.

Task reducer flow: action dispatched → reducer → new state slice ExpandTask reducer flow: action dispatched → reducer → new state slice

The Essentials

  1. 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.
  2. Immutable updates: CREATE_TASK uses spread to return a new array. DELETE_TASK uses filter. Neither modifies the existing state object. Redux detects changes by reference. Mutation makes state appear unchanged.
  3. combineReducers names the state slice. The key you pass (tasks) becomes the property name on the global state. Access task data as state.tasks everywhere in the app.

Further Reading and Watching