Skip to content

Redux - Reducers

Reducers are a fundamental concept in Redux. They are pure functions responsible for specifying how the application’s state changes in response to actions. Given the previous state and an action, a reducer calculates and returns the next state. Because they are pure functions (no side effects, same input always yields same output), they make state changes predictable.

Key principles for reducers:

  • They should never mutate their arguments (the state object or the action).
  • They should never perform side effects like API calls or routing transitions.
  • They should never call non-pure functions (e.g., Date.now() or Math.random()).

The basic signature of a reducer remains (previousState, action) => newState.

Redux Toolkit’s createSlice function significantly simplifies writing reducers:

  • It automatically generates action creators and action types.
  • It uses the Immer library internally, allowing you to write code that looks like direct state mutation, but ensures immutable updates happen under the hood. This makes reducer logic much cleaner and less error-prone.
  • It encourages organizing reducers by feature or ‘slice’ of state.

Let’s revisit the example of managing a list of items, perhaps fetched from an API. Here’s how you might define a slice for this using createSlice:

// features/items/itemsSlice.js
import { createSlice } from '@reduxjs/toolkit';
// Define the initial state for this slice
const initialState = {
list: [],
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
error: null,
};
const itemsSlice = createSlice({
name: 'items',
initialState,
// Define reducers for synchronous actions
reducers: {
// Example: action to add a single item (payload is the item)
itemAdded(state, action) {
// Immer allows direct 'mutation' here
state.list.push(action.payload);
},
// Example: action to clear all items
itemsCleared(state) {
state.list = [];
state.status = 'idle';
state.error = null;
}
// We will add reducers for async actions (like fetching) below
},
// Add extraReducers for actions defined elsewhere, like async thunks
// Often used for handling pending/fulfilled/rejected states of async operations
extraReducers(builder) {
// Example: If using createAsyncThunk for fetching items
/*
builder
.addCase(fetchItems.pending, (state, action) => {
state.status = 'loading';
})
.addCase(fetchItems.fulfilled, (state, action) => {
state.status = 'succeeded';
// Add any fetched items to the array
state.list = state.list.concat(action.payload);
})
.addCase(fetchItems.rejected, (state, action) => {
state.status = 'failed';
state.error = action.error.message;
});
*/
}
});
// Export the generated action creators
export const { itemAdded, itemsCleared } = itemsSlice.actions;
// Export the reducer function itself
export default itemsSlice.reducer;

Notice how the Immer integration within createSlice allows simpler syntax like state.list.push(action.payload) instead of needing manual spread syntax (...state) or Object.assign/concat for immutable updates. The reducers object maps action type names directly to case reducer functions.

For larger applications, you’ll create multiple slices, each managing its own part of the application state. Redux Toolkit’s configureStore function handles combining these slice reducers into a single root reducer for you.

Imagine another slice for user authentication:

// features/auth/authSlice.js
import { createSlice } from '@reduxjs/toolkit';
const initialState = {
user: null,
token: null,
};
const authSlice = createSlice({
name: 'auth',
initialState,
reducers: {
userLoggedIn(state, action) {
state.user = action.payload.user;
state.token = action.payload.token;
},
userLoggedOut(state) {
state.user = null;
state.token = null;
},
},
});
export const { userLoggedIn, userLoggedOut } = authSlice.actions;
export default authSlice.reducer;

When setting up the store, you pass an object containing your slice reducers to configureStore. It automatically calls combineReducers internally.

app/store.js
import { configureStore } from '@reduxjs/toolkit';
import itemsReducer from '../features/items/itemsSlice';
import authReducer from '../features/auth/authSlice';
export const store = configureStore({
// The keys here determine the shape of the state object
reducer: {
items: itemsReducer,
auth: authReducer,
// Add other slice reducers here
},
});
// The resulting state shape will be: { items: { ... }, auth: { ... } }

This approach promotes modularity and makes it easier to manage state logic as your application grows, keeping the core principles of pure functions and predictable state changes central to Redux.