Refactoring a React app to use Redux and Thunk: Part Two, adding Redux

Amber Wilkie
Jul 27, 2017 · 8 min read

In part one, I walked through a simple React app to search NASA and “save” items for later use. All we’re doing is updating the local state of App.js and any components who want to access this information must have it passed down from App. Let’s take some of the burden for that work off of our App.js and bring in Redux.

Setup

First, we’ll install. I used create-react-app for this, so we’ll work with yarn:

yarn add redux react-redux

The store

The first thing we’ll need to do is create our store. The store is a global state that can be accessed by any container. More on that in a second. Here’s how we’ll set up our store. (As a note, unchanged code I’ll stick in [...]. For instance, here we’ve got a bunch of unchanged imports, so I omit them for brevity.)

[...]
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import rootReducer from './reducers';
const store = createStore(rootReducer);ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
registerServiceWorker();

We’ll also need to build that rootReducer:

import { combineReducers } from 'redux'const addOne = (state = 0, action) => {
return state + 1;
}
const addOne = (state = 10, action) => {
return state + 2;
}
export default combineReducers({
addOne,
addTwo
})

We’ve added a dummy reducer, just to understand what is going on briefly. combineReducers is a Redux utility that simply takes multiple reducers and (you guessed it), combines them. You simply list the reducers you want to use inside the curly brackets. By convention, we’ll name the reducer the same thing as the piece of state we want to affect. (Whaaaa? I’ll explain).

State in Redux

Redux manages the entire global state for your application. We call the this global state object the “store”. Individual containers can pluck, at will, the items they need from the global state so that we don’t have to keep passing props all the way down from App.js and we don’t have to spend a great deal of time worrying about which containers / components are going to need which piece of data or method. Since containers have to opt into receiving information / methods from the store, we can hopefully prevent some problems with having everything in a global state.

If you think of the store as an object, then reducers are concerned with updating individual pieces of that object. From our example above, here’s what our store would look like:

{
addOne: 0,
addTwo: 10
}

This part was very confusing for me at first. By convention, we’ll call the first argument to a reducer state and give it an initial value but that is not the total state of the app, it refers only to this individual piece of the state.

Containers vs. Components

When you start using Redux, you will also see a separation of syntax with containers vs. components. For our purposes, a container is a component that has access to the store. It will handle logic and will send props down to the components — which we will consider to be simply presentational. Components don’t have to know anything at all about state — they just manipulate their props to display them. Everything else should be handled by a parent container.

What the heck is a reducer?

So, finally, what are these things? Reducers are responsible for updating the state of the app. They typically wait for a “signal” of sorts (covered next), then update their piece of store based on the signal and the “payload”. Reducers quietly sit around waiting to be called on. When they are, they take in the previous state, make updates, then return an updated state. The store is then updated, and any affected containers and components will re-render.

A Real Reducer

Alright let’s deal with some real code. We are going to refactor out those parts of our state that we need to access from multiple parts of the app. Let’s look first at the navigation, since that is a relatively simple reducer:

const page = (state = 'home', action) => {
switch(action.type) {
case 'CHANGE_PAGE':
return action.payload
default:
return state
}
}

Remember from Part One that we had all kinds of messy if page === 'home' type of stuff going on in App.js. If we can manage the page from the store, we can access it from any container and display the proper information.

Our reducer defaults to 'home' if state is not already set (this happens when the app loads and serves as a fallback). The action will be sent by dispatch, which we will cover in a second. For now, think of CHANGE_PAGE as a global message sent by our app. If that message goes out, the page reducer is going to hear it and perform an action. Here we’re just changing this piece of state to whatever is sent in action.payload. We need a default case because when the app loads, no messages are sent but each reducer is called.

A Real Action

Actions are the means by which reducers are called. They emit the “signal” and define the “payload” for our reducers to receive. Actions are performed by parts of the application, whereas reducers are never directly called — as I said, they just sit around waiting for the signal.

Here’s the action for changing the navigation. Remember that we had this in App.js:

const toggleSavedView = (e) => {
e.preventDefault();
if (this.state.page === 'home') {
this.setState({
page: 'saved'
})
} else {
this.setState({
page: 'home'
})
}
}

That is going to get cumbersome quickly. Here’s our refactored action:

const changePage = page => ({
type: 'CHANGE_PAGE',
payload: page
})
export { changePage };

What we are “dispatching” here becomes our action for the reducer above. Notice the payload — we are going to pass changePage to this action and when it is delivered to the reducer, the store will be updated appropriately.

Getting the action to the container

Now if we want to actually call this action, we’ll do a few things. First, we’ll need to import it to our container. I don’t have many actions for this app, so I left everything in index, but you could give every action its own file, import them into /actions/index.js and then export everything from there. It won’t matter for our example.

import * as actions from './actions';

This line will bring in everything from actions. Since I’m still working with App.js, I need everything — but if your container only needs a few actions, you should definitely import them individually. Now the action will be available as action.page.

mapDispatchToProps

Before we can use the action we’ve imported, we’ll need to let Redux get involved so that the message can be sent across the app. We need a couple more imports:

import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';

bindActionCreators lets us call “dispatch” from the container and connect will allow us to access the store and the actions.

Next, we’ll add our mapDispatchToProps function:

const mapDispatchToProps = dispatch => {
return bindActionCreators({
changePage: actions.changePage
}, dispatch)
}
const App = connect(
null,
mapDispatchToProps
)(NasaApp)

Here we’re going to pass the action changePage to the container as a prop — we’ll be able to access it through this.props.page(). We have to pass null as a first argument to connect — we’ll fix that in a sec (it will throw errors otherwise). Finally, we’re wrapping up our app in connect but we want to keep the export name the same, so we’ll have to re-name the container. I called it NasaApp. Now our app is connected to dispatch!

Lastly, we need to call this action when we click the Home/Saved links so we can use it:

const toggleSavedView = (e, page) => {
e.preventDefault();
this.props.changePage(
e.target.href.replace('http://localhost:3000/', '')
);
}
[...]
{ this.state.page === 'home' &&
<a href='/saved' className='orange-link'
onClick={(e) => toggleSavedView(e, 'saved')}>Saved</a> }
{ this.state.page === 'saved' &&
<a href='/home' className='orange-link'
onClick={(e) => toggleSavedView(e, 'home')}>Home</a> }

We’re grabbing the URL href so that we don’t have to explicitly pass the 'home' and 'save' or whatever. This makes the function more flexible. Obviously if we change our URL, we’ll have to update this method. (This is all still pretty messy, but we’ll clean it up in a coming post with React Router).

Awesome, right?! But try clicking the link — it doesn’t work. That’s because, even though we are updating our store, our container doesn’t know anything about it. We are still relying on the local state, which stays 'home' no matter what we do with dispatch. Let’s set our container to care about the store.

mapStateToProps

This step is much shorter. We’ll add our mapStateToProps to App.js so that we can access the items in the store:

const mapStateToProps = (state = {}, ownProps) => {
return {
page: state.page,
}
}

That state argument comes from the store. If we don’t get anything from the store, it will default to an empty object. Then we’ll send page to props so that we can use that data. We now have this.props.page to use and we can update our JSX to use that instead of this.state.page. And boom — we are using Redux! Our app navigates the way it used to. But now any container can have access to the page and update the page we are on using changePage.

Save Item reducer and action

One cool thing about reducers is that they don’t care what the other reducers are up to. That means you can have a bunch of reducers listening for the same message. Any reducer that “listens” for CHANGE_PAGE will execute when that “signal” is sent by the action.

First, let’s destructure our actions and props so that we don’t have to keep typing this.props or action.whatever:

const {
page,
addNasaItem,
} = this.props;

(this needs to go after render{ but before return().

const {
changePage,
addNasaItem,
} = actions;

(this can go inside mapDispatchToProps before the return statement). Nice! Much less typing for us and code to read for anyone who comes after us.

Saving Items

Our app currently handles saving items by storing them in the App.js local state. This gives us limited ability to access them otherwise. Let’s refactor that into a reducer. First, the action we want to dispatch when we are saving an item:

const addNasaItem = item => ({
type: 'ADD_ITEM',
payload: item
})

Don’t forget to export it from /actions/index.js. Add it to our mapDispatchToProps:

const mapDispatchToProps = dispatch => {
const {
changePage,
addNasaItem,
} = actions;
return bindActionCreators({
changePage,
addNasaItem
}, dispatch)
}

Note that with our destructuring, we can also destructure our arguments to bindActionCreators —everything is starting to look pretty neat and trim.

Now we can use the action in our code:

const handleSave = (item) => {
addNasaItem(item);
}

Great! Now we’re saving items in the store. We need to read the saved property from the store and pass it to ResultsList and we’ll finally have transferred that functionality to Redux. Add saved to our destructured props and then

<ResultsList saved={saved}
results={this.state.results}
handleSave={handleSave} />

Update the ResultsList for both home and saved pages and now you’re seeing the saved items (and count) from the store. We’re getting somewhere.

Once again, this tutorial has gone on far longer than I intended, so I’m going to cut it off. In the next one, we’ll continue refactoring out our functionality so that Redux can handle all the actions we need to take.

Amber Wilkie

Written by

Software developer, mostly Ruby and Javascript. Yogi, Traveler, Enthusiast. All photographs mine. I don’t read the comments — try me on Twitter.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade