Reducing Reducers: The Interplay of State, Actions and Reducers in React-Redux

R. Cory Stine
8 min readJan 17, 2022

At first glance, Redux can feel like an added layer of complexity on top of the already complex React framework, especially when compared to vanilla JavaScript. It is an abstraction of an abstraction, which can be confusing and intimidating.

However, once you get a grasp on their idiosyncrasies, React and Redux become invaluable tools that help to streamline your code, offering a modularity that often feels more organized and readable. In particular, I’ve found React’s use of declarative programming — allowing you to state what the desired results of your code are —to be more intuitive and easier to visualize than traditional imperative coding. Combined with Redux’s ability to store our data in a single JavaScript Object outside of our Components, we have access to powerful tools that allow those Components to interact and collaborate in more effective ways.

But even with all these benefits, there is still one major concept that I struggle to wrap my head around: reducers. What exactly is a reducer? What is it doing for us? And why are they so crucial for the most effective Redux pattern? Let’s take a deeper dive.

A reducer is a function that takes in the current state, as well as an action that we would like to exercise on that state, returning a new, updated (or reduced) version of that state in a single value. One of the ways I’ve learned to think about reducers in Redux is to consider the .reduce() method in JavaScript. Considering the naming convention, it is no surprise that they basically do the same thing!

const numbers = [1, 2, 3, 4, 5]const sum = (prevValue, currentValue) => prevValue + currentValue;numbers.reduce(sum)
//output=> 15

In this example, our numbers variable would be similar to our current state, an array of numbers ordered sequentially from 1–5. Our sum function is the action we would like to engage in: adding each value to the previous value and totaling the answer into a sum of the array. When we run .reduce() on our current state (numbers) and pass in our action (sum), a total value of 15 is returned — equivalent to our updated state.

Let’s compare our analogy to how this actually works in Redux, using an example from my project.

###src/reducers/searchReducer.jsconst initState = {
value: ""
}
const searchReducer = (state = initState, action) => {
switch(action.type){
case "UPDATE_SEARCH_FORM":
return action.searchData
case "RESET_SEARCH_FORM":
return initState;
default:
return state;
}
}
export default searchReducer;

This is the reducer I use to update the text in my search form, as well as reset it when it isn’t being used. The search reducer function has two arguments passed in: state (set to an initial state with an empty value element) and action.

But what comes next?

The switch statement is touched on only briefly in the Flatiron curriculum and was a big hurdle to my understanding of the structure of a reducer. But in reality, switch is simply another form of conditional statement in JavaScript, not unlike the more common if/then pattern. Switch evaluates a passed in expression, which is compared to the case values below it. If the expression matches the case value, the block of code within that case is run. The default case runs if the expression doesn’t match any of the other cases.

Switch is a great solution for situations like this, where you have a potential for a long string of if/then statements that could get unwieldy.

With this information in hand, we can see that the switch statement in my code evaluates action.type. If the action.type is “UPDATE_SEARCH_FORM”, then I return action.searchData. If, instead, it is “RESET_SEARCH_FORM”, I return the initial state. And if it is neither, I return the default of state. This perfectly describes how switch is being used as a conditional.

Where are these expressions coming from though? What is my action?

An action is a JavaScript object that describes something you would like to happen in an application. It has a type field to pair it with a reducer case type. If we take a look at the searchForm file in my actions folder, you can see the answer to the above questions.

###src/actions/searchForm.jsexport const updateSearchForm = searchData => {
return {
type: "UPDATE_SEARCH_FORM",
searchData
}
}
export const resetSearchForm = () => {
return {
type: 'RESET_SEARCH_FORM'
}
}

Contained within are two functions. The first, updateSearchForm, passes in searchData from the form, then returns a type of “UPDATE_SEARCH_FORM” as well as a payload of searchData. The second, resetSearchForm simply return the type ‘RESET_SEARCH_FORM’, since all this action is doing is clearing any entered form data via my initial state.

In conjunction with my reducers, we can see a clearer picture of what is happening. When I call updateSearchForm in my code, the action.type passed into my reducer’s switch statement will evaluate that the type “UPDATE_SEARCH_FORM” is true — which will then trigger the associated case, returning action.searchData. And where does searchData come from? Our payload in the action creator.

This is how reducers interact with actions to change state. But to close out, we need to call our action. In the case of updateSearchData, the best place to do that is in my SearchForm component.

This is where everything comes together.

###src/components/SearchForm.jsimport React from 'react';
import { connect } from 'react-redux';
import { updateSearchForm } from '../actions/searchForm';
const SearchForm = ({ searchReducer, updateSearchForm }) => {
const handleOnChange = event => {
event.preventDefault()
const value = event.target.value
const updatedData = {
...searchReducer,
value
}
updateSearchForm(updatedData)
}
return (
<div className="search">
<form>
<div className="form-group">
<input
type="text"
className="form-control"
onChange={handleOnChange}
placeholder="Search..."
value={searchReducer.value}
/>
</div>
</form>
</div>
)}
const mapStateToProps = state => {
return {
searchReducer: state.searchReducer
}
}
export default connect(mapStateToProps, { updateSearchForm })(SearchForm);

There is a lot going on here, so we’re going to stick to what is pertinent to this discussion.

At the top of the component, I import the updateSearchForm action from the appropriate actions file. At the bottom, I use connect to deconstruct updateSearchForm and map it to dispatch, giving me access to use it later in my code. I’ve also mapped my searchReducer to props to get access to the value state so that it can be updated.

In the SearchForm component function, I pass in my destructured searchReducer and updateSearchForm, giving me access to my state and my action. Knowing that I want to update my value element in state when the input into my form changes, I declare a function called handleOnChange with the explicit purpose of calling updateSearchForm.

Before I can do that, I need the data I intend to pass in. Using event.target.value, I grab the entered text from my search form and assign it to a constant called value (to match with my state element). That data is assigned to a new object called updatedData, alongside the original information from searchReducer, which gives us our updated text to dispatch as an argument. Finally, we call our updateSearchForm action, passing in our updatedData. In our JSX code to render the form, we call our handleOnChange method when the search form is changed in any way. Interacting with our component and our actions, the reducer officially works!

To summarize everything we’ve learned, let’s break it down using a specific example in my application: BoardGameShelf.

In the search form, which is rendered by JSX in my SearchForm component, I have entered the text “Gloomhaven” — and you can see that we have received a result. Cool! Let’s follow that down to my reducer.

Since I changed the text in the search bar, this triggered my handleOnChange function, which grabs the data from the input using event.target.value and passesthat updated value into our updateSearchForm action. In pseudocode, it would look like this:

updateSearchForm({ value: 'Gloomhaven' });

With updateSearchForm called, we return the action creator with a type of “UPDATE_SEARCH_FORM” and a payload of the searchData we passed in through handleOnChange. Again, the pseudocode:

export const updateSearchForm = ({ value: 'Gloomhaven' }) => {
return {
type: "UPDATE_SEARCH_FORM",
searchData: ({ value: 'Gloomhaven' })
}
}

Since our type is “UPDATE_SEARCH_FORM”, the switch in our searchReducer evaluates our action and knows to call the code in the associated case. Here, it will return action.searchData. Just a reminder of the structure of our original reducer:

const initState = {
value: ""
}
const searchReducer = (state = initState, action) => {
switch(action.type){
case "UPDATE_SEARCH_FORM":
return action.searchData
case "RESET_SEARCH_FORM":
return initState;
default:
return state;
}
}
export default searchReducer;

Back at the beginning, I mentioned that a reducer is not all too different from the .reduce() method in JavaScript. With all of our data lined up to search for “Gloomhaven”, we’re finally able get our reduced value. One last bit of pseudocode:

searchReducer(state = {value: ""}, action = {value: "Gloomhaven"}}
//output=> {value: "Gloomhaven"}

Our state element - value - has officially been changed to reflect the text entered into the form using our reducer. It’s even reflected in our Redux devtools.

It is not hard to see the power of reducers when it is laid out like this. They are an amazing means of changing state in an organized, efficient manner. Though reducers can be a bit complex to learn, they can also be demystified to enhance your own React-Redux projects. If I have learned anything in my time at Flatiron, it is that every challenge can be overcome if it is broken down into individual parts.

To see more examples of reducers, check out the Github for BoardGameShelf, located here:

--

--