Redux - Reducers
Redux - Reducers with Redux Toolkit
Section titled “Redux - Reducers with Redux Toolkit”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()orMath.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 sliceconst 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 creatorsexport const { itemAdded, itemsCleared } = itemsSlice.actions;
// Export the reducer function itselfexport 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.
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.