Redux Fundamentals: Reducers
Reducers are the heart of Redux's state management. They are pure functions that determine how the application's state changes in response to actions. Let's break down what that means and how to implement them.
What are Reducers?
- Pure Functions: This is crucial. A pure function has these characteristics:
- Deterministic: Given the same input, it always returns the same output.
- No Side Effects: It doesn't modify anything outside of its own scope (no API calls, no direct DOM manipulation, no modifying input arguments).
- State Transformation: Reducers take the current state and an action as input and return the next state. They don't modify the existing state; they create a new state object.
- Action Driven: Reducers react to actions dispatched by your components. The action's
typeproperty tells the reducer what kind of state change is requested.
Reducer Structure
A typical reducer looks like this:
function reducer(state = initialState, action) {
switch (action.type) {
case 'ACTION_TYPE_1':
// Logic to update state for ACTION_TYPE_1
return newState;
case 'ACTION_TYPE_2':
// Logic to update state for ACTION_TYPE_2
return newState;
default:
// If the action type is not recognized, return the current state.
return state;
}
}
state: The current state of the application (or a slice of it). It's good practice to provide a defaultinitialStatein case the state is undefined.action: An object describing what happened. It must have atypeproperty. It can also contain apayloadwith data needed to update the state.switchstatement: This is the most common way to handle different action types.return newState;: Crucially, you return a new state object. Never modify the originalstate.default:: This is essential. If the reducer receives an action it doesn't recognize, it must return the current state unchanged. This prevents unexpected behavior.
Immutability: Why it Matters
Redux relies heavily on immutability. Here's why:
- Predictability: Immutable state makes it easier to reason about your application's behavior. You can be confident that the state won't change unexpectedly.
- Change Detection: Redux uses strict equality (===) to determine if the state has changed. If you modify the state directly, Redux won't detect the change, and your components won't re-render.
- Debugging: Immutability makes debugging much easier. You can track state changes over time and easily revert to previous states.
Updating State Immutably
Here are common techniques for updating state immutably:
Object Spread Operator (
...): Creates a shallow copy of an object.const newState = { ...state, propertyToUpdate: newValue };Array Spread Operator (
...): Creates a shallow copy of an array.const newState = { ...state, myArray: [...state.myArray, newItem] };Array.map(): Creates a new array by applying a function to each element of the original array.const newState = { ...state, myArray: state.myArray.map(item => { if (item.id === action.payload.id) { return { ...item, propertyToUpdate: newValue }; } return item; }) };Array.filter(): Creates a new array containing only the elements that pass a test.const newState = { ...state, myArray: state.myArray.filter(item => item.id !== action.payload.id) }; // Remove an itemLibraries like Immer: Immer simplifies working with immutable data by allowing you to write mutable-looking code that is automatically converted to immutable updates. (More advanced, but very helpful for complex state structures).
Example Reducer
Let's say we have a counter application.
const initialState = {
count: 0
};
function counterReducer(state = initialState, action) {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + 1 };
case 'DECREMENT':
return { ...state, count: state.count - 1 };
default:
return state;
}
}
export default counterReducer;
Combining Reducers
For larger applications, you'll likely have multiple reducers, each responsible for a different part of the state. Redux provides the combineReducers function to combine these into a single root reducer.
import { combineReducers } from 'redux';
import counterReducer from './counterReducer';
import userReducer from './userReducer';
const rootReducer = combineReducers({
counter: counterReducer,
user: userReducer
});
export default rootReducer;
Now, the state will be structured like this:
{
counter: { count: ... },
user: { ... }
}
Key Takeaways
- Reducers are pure functions that update state based on actions.
- Immutability is essential for predictable state management.
- Use the spread operator,
map,filter, or libraries like Immer to update state immutably. combineReducersallows you to split your state into manageable slices.- Always handle the
defaultcase in yourswitchstatement.