Skip to main content

Redux Reducers (Part 1): Handling Actions and Updating State #109

?? Introduction

In Redux, reducers are where state transitions actually happen.

Actions tell us what occurred. The store coordinates update flow. But reducers are the rules engine: given previous state and an action, they return the next state.

This sounds mechanical, yet reducer quality has huge impact on app reliability. Good reducers are predictable, testable, and boring in the best possible way. Bad reducers hide mutation, couple unrelated concerns, and create subtle rendering bugs.

In this lesson, we will build intuition for reducer design with realistic examples and immutable update techniques you can use immediately in React projects.


?? Prerequisites

Before this article, review:

You should be familiar with action type and payload structure.


?? Article Outline: What You'll Master

In this lesson, you will learn to:

  • Define reducers as pure, deterministic functions.
  • Handle multiple action types in one feature reducer.
  • Perform immutable updates for arrays and nested objects.
  • Avoid common reducer mistakes that break React rendering.

?? Section 1: Reducer Fundamentals

A reducer has this signature:

function reducer(state = initialState, action) {
// return next state
}

Reducer rules:

  • Must be pure: same inputs ? same output.
  • Must not mutate existing state.
  • Must return current state for unknown actions.
  • Should contain transition logic, not side effects (no API calls, no timers).

A feature reducer example:

const initialState = {
items: [],
status: 'idle',
};

function cartReducer(state = initialState, action) {
switch (action.type) {
case 'cart/itemAdded': {
const id = action.payload.id;
const existing = state.items.find(item => item.id === id);

if (existing) {
return {
...state,
items: state.items.map(item =>
item.id === id ? { ...item, quantity: item.quantity + 1 } : item
),
};
}

return {
...state,
items: [...state.items, { id, quantity: 1 }],
};
}

default:
return state;
}
}

?? Section 2: Immutable Updates Without Pain

Most reducer bugs come from accidental mutation.

Incorrect:

state.items.push({ id: 'p7', quantity: 1 });
return state;

This mutates the existing array. React Redux may fail to detect a meaningful reference change where expected, and historical state snapshots become unreliable.

Correct pattern:

  • arrays: map, filter, spread syntax.
  • objects: shallow copy with { ...state } and nested copies as needed.

Nested update example:

case 'profile/themeChanged':
return {
...state,
preferences: {
...state.preferences,
theme: action.payload.theme,
},
};

Each updated level gets a new object reference.


?? Section 3: Reducer Scope and Design

Keep reducers focused by feature. A cartReducer should not process authentication transitions, and vice versa.

In larger apps, split reducers and combine them into one root reducer. This supports modular code and clear ownership per domain.

import { combineReducers } from 'redux';
import authReducer from './authReducer';
import cartReducer from './cartReducer';

export const rootReducer = combineReducers({
auth: authReducer,
cart: cartReducer,
});

Now each reducer receives only its slice, which simplifies logic and testing.


?? Section 4: Testing Reducer Behavior

Reducer tests should verify transitions, not implementation details.

test('adds a new item with quantity 1', () => {
const state = { items: [] };
const action = { type: 'cart/itemAdded', payload: { id: 'p1' } };

const next = cartReducer(state, action);

expect(next.items).toEqual([{ id: 'p1', quantity: 1 }]);
expect(next).not.toBe(state);
});

These tests are fast and give confidence during refactors.


?? Common Mistakes

  • Mutating nested objects directly.
  • Returning undefined for unknown actions.
  • Performing async calls in reducers.
  • Storing derived values that should be selectors instead.

When logic grows complex, move repetitive update rules into helper functions, but keep reducers explicit enough that teammates can follow transitions quickly.


?? Conclusion & Key Takeaways

Reducers are the heart of Redux predictability. They enforce a strict transition model: previous state + action ? next state. If you keep reducers pure and immutable, React updates become easier to reason about and bugs become easier to isolate.

Key takeaways:

  • Reducers define deterministic state transitions.
  • Immutability is mandatory, not optional.
  • Feature-based reducer boundaries improve maintainability.
  • Reducer tests are low-effort, high-value safety nets.

?? Next Steps

Continue to Redux Reducers (Part 2), where we will apply these patterns to a larger reducer and compare manual updates with Redux Toolkit�s Immer-powered approach.


glossary

  • Reducer: Pure function that calculates next state from current state and action.
  • Pure Function: Function without side effects, deterministic output for given input.
  • Immutable Update: Creating new objects/arrays instead of mutating existing ones.
  • Root Reducer: Combined reducer mapping feature slices to feature reducers.
  • State Transition: One discrete movement from previous to next state.

Further Reading