A reducer is a function that determines changes to an application’s state. It uses the action it receives to determine this change. We have tools, like Redux, that help manage an application’s state changes in a single store so that they behave consistently.
Why do we mention Redux when talking about reducers? Redux relies heavily on reducer functions that take the previous state and an action in order to execute the next state.
We’re going to focus squarely on reducers is in this post. Our goal is to get comfortable working with the reducer function so that we can see how it is used to update the state of an application — and ultimately understand the role they play in a state manager, like Redux.
What we mean by “state”
State changes are based on a user’s interaction, or even something like a network request. If the application’s state is managed by Redux, the changes happen inside a reducer function — this is the only place where state changes happen. The reducer function makes use of the initial state of the application and something called action, to determine what the new state will look like.
If we were in math class, we could say:
initial state + action = new state
In terms of an actual reducer function, that looks like this:
const contactReducer = (state = initialState, action) => {
// Do something
}
Where do we get that initial state and action? Those are things we define.
The state parameter
The state
parameter that gets passed to the reducer function has to be the current state of the application. In this case, we’re calling that our initialState
because it will be the first (and current) state and nothing will precede it.
contactReducer(initialState, action)
Let’s say the initial state of our app is an empty list of contacts and our action is adding a new contact to the list.
const initialState = {
contacts: []
}
That creates our initialState
, which is equal to the state
parameter we need for the reducer function.
The action parameter
An action
is an object that contains two keys and their values. The state update that happens in the reducer is always dependent on the value of action.type
. In this scenario, we are demonstrating what happens when the user tries to create a new contact. So, let’s define the action.type
as NEW_CONTACT
.
const action = {
type: 'NEW_CONTACT',
name: 'John Doe',
location: 'Lagos Nigeria',
email: '[email protected]'
}
There is typically a payload
value that contains what the user is sending and would be used to update the state of the application. It is important to note that action.type
is required, but action.payload
is optional. Making use of payload
brings a level of structure to how the action object looks like.
Updating state
The state is meant to be immutable
, meaning it shouldn’t be changed directly. To create an updated state, we can make use of Object.assign
or opt for the spread operator.
Object.assign
const contactReducer = (state, action) => {
switch (action.type) {
case 'NEW_CONTACT':
return Object.assign({}, state, {
contacts: [
...state.contacts,
action.payload
]
})
default:
return state
}
}
In the above example, we made use of the Object.assign()
to make sure that we do not change the state value directly. Instead, it allows us to return a new object which is filled with the state that is passed to it and the payload sent by the user.
To make use of Object.assign()
, it is important that the first argument is an empty object. Passing the state as the first argument will cause it to be mutated, which is what we’re trying to avoid in order to keep things consistent.
The spread operator
The alternative to object.assign()
is to make use of the spread operator, like so:
const contactReducer = (state, action) => {
switch (action.type) {
case 'NEW_CONTACT':
return {
...state, contacts:
[...state.contacts, action.payload]
}
default:
return state
}
}
This ensures that the incoming state stays intact as we append the new item to the bottom.
Working with a switch statement
Earlier, we noted that the update that happens depends on the value of action.type
. The switch statement conditionally determines the kind of update we’re dealing with, based on the value of the action.type
.
That means that a typical reducer will look like this:
const addContact = (state, action) => {
switch (action.type) {
case 'NEW_CONTACT':
return {
...state, contacts:
[...state.contacts, action.payload]
}
case 'UPDATE_CONTACT':
return {
// Handle contact update
}
case 'DELETE_CONTACT':
return {
// Handle contact delete
}
case 'EMPTY_CONTACT_LIST':
return {
// Handle contact list
}
default:
return state
}
}
It’s important that we return state our default
for when the value of action.type
specified in the action object does not match what we have in the reducer — say, if for some unknown reason, the action looks like this:
const action = {
type: 'UPDATE_USER_AGE',
payload: {
age: 19
}
}
Since we don’t have this kind of action type, we’ll want to return what we have in the state (the current state of the application) instead. All that means is we’re unsure of what the user is trying to achieve at the moment.
Putting everything together
Here’s a simple example of how I implemented the reducer function in React.
See the Pen
reducer example by Kingsley Silas Chijioke (@kinsomicrote)
on CodePen.
You can see that I didn’t make use of Redux, but this is very much the same way Redux uses reducers to store and update state changes. The primary state update happens in the reducer function, and the value it returns sets the updated state of the application.
Want to give it a try? You can extend the reducer function to allow the user to update the age of a contact. I’d like to see what you come up with in the comment section!
Understanding the role that reducers play in Redux should give you a better understanding of what happens underneath the hood. If you are interested in reading more about using reducers in Redux, it’s worth checking out the official documentation.
I am curios about the following questions:
how do you organize multiple reducers in a big app?
should all state go to the redux state? or is there a way to have a balanced mix of redux and local state? what are do’s and dont’s?
do you know any examples of big open source apps written with Redux?
Personally for me (using Vue/Vuex, but similar concepts for state).
Anything that could be considered global, (user account information, notifications, etc) goes into the store.
Anything that isn’t “global” but is shared between 2 or more components that aren’t direct parent/child relationships also goes into the store.
Anything that is completely contained inside a single component goes into local state.
For anything that falls outside of those categories, the fetal position and a good 30-minute crying session does the trick.
You can create multiple reducers, divided by logical sections in your application. Redux provides a combineReducers method that does exactly what the name implies.
You can visit a larger React site with the Redux Developer extension for the browser to see how other sites split up their reducers.
Also, I’m a big fan of putting most data in Redux because it can be used as a cache. If you store data locally it will be lost on cDU. You can cache it with localstorage, but with Redux the data is preserved between routechanges.
It comes down to personal preference. I work in a React agency and this is a discussion we have over and over again.
This is a really well laid out article. I loved how your broke down the reducer function in isolation rather than specifically in context to React’s useReducer or Redux. This is article fills a real void and will be sharing it with some of my team who are struggling with the reducer context. So major kudos for this.
However, I do have a minor point of contention. The dependency on action.type is a best practice and common convention, but could technically be replaced by any property or even switch based on a second action parameter. I am only calling this out, because one of the most difficult things I struggled when learning the reducer was learning what was dogma requirements for it to work, and what was common convention.
Along with my minor contention, I also present a different way for handling the action type.
By using an object over a switch statement can help deal with some of the clunkier aspects of switch (any nested conditional logic, let and const in more complicated cases, etc).