The complete beginners guide to Redux reducers with code examples
As I was researching & learning React, one of the problems that was introduced to me very quickly was managing React state across my application.
A lot of the tutorials back in the day recommended to store all your React state on your top level component.
But managing all that state logic is crazy talk. Throughout my researching the word Redux kept getting tossed around.
Redux, a state management library, has 3 core principles.
- Single source of truth
- State is read-only
- Changes are made with pure function called reducers
Bullets 1 & 2 made sense. But bullet 3, reducers?
Are they talking about Array.reducers()?
Here were my beginner questions when learning Redux reducers:
- What is a reducer?
- Should all state go into Redux state?
- How do I trigger an action?
- Can you have multiple reducers?
- What are some do’s and dont’s for Redux reducers?
The definition of a reducer in Redux
According to Redux docs, a reducer is a function that accepts an accumulation, new values, and returns a new accumulation.
These types of functions are also called reducing functions.
This technique of returning a new value based off a collection of data is not new. In fact, it sounds a lot like Array.reduce().
Let’s look at a example of what a reducer function may look like.
const INITIAL_STATE = Object.freeze({
name: 'Jon Grumpy',
mood: 'pissed',
greetMessage: 'What are you looking at?'
});
function someReducer(state = INITIAL_STATE, action) {}
In Redux, you should always have a initial state object.
As you can see above, someReducer() is accepting 2 arguments:
- state – The accumulation
- action – New values
Okay let’s add a bit more logic to this reducer function.
function someReducer(state = INITIAL_STATE, action) {
switch(action.type) {
case 'CHANGE_MOOD':
return {
...state,
mood: action.payload,
};
case 'CHANGE_GREET_MESSAGE':
return {
...state,
greetMessage: action.payload,
};
default:
return state;
}
}
The logic is pretty simple. It’s a simple switch statement that checks the value of the action type.
If it matches any of the switch case statements it will return a new object with the previous state object data, and the new updated values for mood or greetMessage.
If the action type value does not match any of the cases, then it just returns the current state object.
In layman term, a reducer function is just a switch statement.
DO NOT put all of your React state in Redux
What kind of data goes into Redux? Should I put all my React state in Redux?
These questions get brought up frequently at work. My answer to this is, no.
Redux is already complicated, and stuffing it with data (UI state) such as, showModal or showDropdown, will just add more confusion, and complexity.
Keep Redux simple for your sake, and future engineers that are going to work with this code.
My rule of thumb is, only add data to Redux if:
- Other parts of my application care about this data
- It adds value to restore the state to a given point in time
- I want to cache the data
I’m a strong believer that using local component state as much as possible, is really good.
As an engineers, it’s our job to determine what state make up our application, and we where it should live.
Find a balance that works for you, and your team.
This is just my opinion. There is no right answer to this question.
Use Redux dispatch to trigger an action
To make proper state modifications in Redux, we must use a function called dispatch().
dispatch() is a lot like React setState(). Redux does not allow you to modify the state object directly, and requires you to only modify it through a function.
dispatch() will create a queue, just like setState() or useState(), and trigger your reducer function accordingly.
Let’s see how this works with code. The first step is to connect your component with Redux.
I will achieve this by using a HOC (higher order component) called connect(). This is provided from the node module, react-redux.
import React from 'react';
import { connect } from 'react-redux';
class ComponentA extends React.Component {
render() {
return (
<>
Name: {' '}
Mood: {' '}
{''}
)
}
}
export default connect()(ComponentA);
In the code example above, I’m invocating connect twice. The first invocation, I’m not passing any arguments. The second invocation, I’m passing down my React component to be connected to my Redux store.
By passing no arguments to the connect() the first time, it will not pass any of the store object data.
But it will pass down the dispatch() function as a part of the props object.
Let’s use the function to change Jon Grumpy’s mood.
class ComponentA extends React.Component {
componentDidMount() {
this.props.dispatch({ type: 'CHANGE_MOOD', payload: 'happy' });
}
render() {
// ... render code
}
}
We can clean this code up a bit and make it a bit more declarative.
I will create a new variable right above the export statement, that has actions to update the Redux store.
const mapDispatchToProps = dispatch => ({
makeJonHappy: () => dispatch({ type: 'CHANGE_MOOD', payload: 'happy' }),
saySomethingNiceJon: () => dispatch({
type: 'CHANGE_GREET_MESSAGE',
payload: 'Nice to meet you...'
}),
});
export default connect(null, mapDispatchToProps)(ComponentA);
I also, passed down my created variable, mapDispatchToProps, as the second argument.
Now I will update the React component.
class ComponentA extends React.Component {
componentDidMount() {
this.props.makeJonHappy();
this.props.saySomethingNiceJon();
}
render() {
// ... render code
}
}
To complete this small example, I will print out the values.
The first step is to create a new variable that returns all the Redux state values and attach it to the component props object.
const mapStateToProps = state => ({ ...state });
export default connect(mapStateToProps, mapDispatchToProps)(ComponentA);
The variable gets tossed down as the first argument.
class ComponentA extends React.Component {
componentDidMount() {
// ... Trigger actions after component has mounted.
}
render() {
return (
<>
<h1>Name: {this.props.name}</h1>
<p>Mood: {this.props.mood}</p>
<p>{this.props.greetMessage}</p>
)
}
}
The final step is to update the render() method, and print out the values from the Redux store.
Setting up for multiple reducers
As I was building complete React application, one my main concerns was learning how to organize a lot of state data with Redux.
At the time, seeing a huge reducer function seemed like a nightmare to maintain.
But Redux has a great little tool to allow us to split reducers into smaller chunks, and combine them into 1.
Let’s take a look at a simple, and basic store.
import { createStore } from 'redux';
const INITIAL_STATE = Object.freeze({
name: 'Jon Grumpy',
mood: 'pissed',
greetMessage: 'What are you looking at?'
});
function someReducer(state = INITIAL_STATE, action) {
//... reducer function logic
}
const store = createStore(someReducer);
export default store;
The store above has a single reducer that describes a single person, their mood, and their greeting message.
But let’s add a bit more, with multiple users, and more greeting messages.
So in the next example, I will show you how to create multiple reducers, and how to combine multiple reducers into one.
In the root of the project directory I will create a new directory called reducers.
This is a common practice to store multiple Redux stores.
The first reducer I will create is hold multiple users. This file will be called reducers/users.js
// reducers/users.js
import { createReducer } from '../store';
// Initial state for users reducers.
// We only got Jon Grumpy in our list, sorry.
const initialState = Object.freeze({
users: [
{ name: 'Jon Grumpy', mood: 'angry', },
],
});
// Identifiers are the action name
const actions = Object.freeze({
ADD_PERSON: addPerson,
});
function addPerson(state, action) {
return {
users: [
...state,
{...action.payload},
]
}
}
export default createReducer(initialState, actions);
Okay now I will create a new reducer file that holds different greeting messages for different moods.
// reducers/moods.js
import { createReducer } from '../store';
// 4 different moods with an array of messages.
const initialState = Object.freeze({
messages: {
happy: [
'Hi there, nice to meet you!',
'Wish you a happy work week!',
'Have a kick ass day :)',
],
sad: [
'waaaahhhh',
'I miss you :(',
'No one understands me'
],
angry: [
'Do not look at me',
'Go f*** yourself..',
'You picked the wrong house, bub.'
],
fearful: [
'Will I be okay?',
'I do not know if I will succeed',
'I am not strong enough'
]
}
})
// No actions allowed for this reducer.
const actions = Object.freeze({});
export default createReducer(initialState, actions);
Now let’s go into our store.js file, combine these reducers together and create the Redux store.
// store.js
import { createStore, combineReducers } from 'redux';
import moodReducer from './reducers/moods.js';
import userReducers from './reducers/users.js';
// Utility function to create a reducers
export function createReducer(initialState, handlers) {
return function reducer(state = initialState, action) {
if (handlers.hasOwnProperty(action.type)) {
return handlers[action.type](state, action);
} else {
return state;
}
}
}
const rootReducer = combineReducers({
moods: moodReducer,
users: userReducer,
});
export default createStore(rootReducer);
Do’s and dont’s for Redux reducers
To finalize this article, I’ll cover some do’s and dont’s that I’ve learned across my journey of learning Redux reducers.
Do’s
- Reducers should only depend on their state and action arguments.
- Store your reducers in a directory called reducers.
- Add as much logic as possible inside the reducer function
- Minimize the use of blind spreads
- Keep your state as flat as possible
- Always return a new object
- Create multiple small reducers for organization & simplicity
- Use combineReducers() to group together multiple reducers
- Write meaningful action names
- Allow many reducers to respond to the same action
- Avoid dispatching many actions sequentially
Dont’s
- Do NOT do any asynchronous logic inside reducer functions
- Do NOT modify global variables inside the reducer function
- Do NOT generate random values
- Do NOT add form data state
- Do NOT create a single large reducer
- Do NOT add state data if other parts of the application don’t care about it
- Do NOT modify the state object directly
I like to tweet about Redux and post helpful code snippets. Follow me there if you would like some too!