A Dummy’s Guide to Redux and Thunk in React

If, like me, you’ve read the Redux docs, watched Dan’s videos, done Wes’ course and still not quite grasped how to use Redux, hopefully this will help.

It took me a few attempts at using Redux before it clicked, so I thought I’d write down the process of converting an existing application that fetches JSON to use Redux and Redux Thunk. If you don’t know what Thunk is, don’t worry too much, but we’ll use it to make asynchronous calls in the “Redux way”.

This tutorial assumes you have a basic grasp of React and ES6/2015, but it should hopefully be easy enough to follow along regardless.

The non-Redux way

Let’s start with creating a React component in components/ItemList.js to fetch and display a list of items.

Laying the foundations

First we’ll setup a static component with a state that contains various items to output, and 2 boolean states to render something different when it's loading or errored respectively.

import React, { Component } from 'react';
class ItemList extends Component {
constructor() {
super();
        this.state = {
items: [
{
id: 1,
label: 'List item 1'
},
{
id: 2,
label: 'List item 2'
},
{
id: 3,
label: 'List item 3'
},
{
id: 4,
label: 'List item 4'
}
],
hasErrored: false,
isLoading: false
};
}
    render() {
if (this.state.hasErrored) {
return <p>Sorry! There was an error loading the items</p>;
}
        if (this.state.isLoading) {
return <p>Loading…</p>;
}
        return (
<ul>
{this.state.items.map((item) => (
<li key={item.id}>
{item.label}
</li>
))}
</ul>
);
}
}
export default ItemList;

It may not seem like much, but this is a good start.

When rendered, the component should output 4 list items, but if you were to set isLoading or hasErrored to true, a relevant <p></p> would be output instead.

Making it dynamic

Hard-coding the items doesn’t make for a very useful component, so let’s fetch the items from a JSON API, which will also allow us to set isLoading and hasErrored as appropriate.

The response will be identical to our hard-coded list of items, but in the real world, you could pull in a list of best-selling books, latest blog posts, or whatever suits your application.

To fetch the items, we’re going to use the aptly named Fetch API. Fetch makes making requests much easier than the classic XMLHttpRequest and returns a promise of the resolved response (which is important to Thunk). Fetch isn’t available in all browsers, so you’ll need to add it as a dependency to your project with:

npm install whatwg-fetch --save

The conversion is actually quite simple.

  • First we’ll set our initial items to an empty array []
  • Now we’ll add a method to fetch the data and set the loading and error states:
fetchData(url) {
this.setState({ isLoading: true });
    fetch(url)
.then((response) => {
if (!response.ok) {
throw Error(response.statusText);
}
            this.setState({ isLoading: false });
            return response;
})
.then((response) => response.json())
.then((items) => this.setState({ items })) // ES6 property value shorthand for { items: items }
.catch(() => this.setState({ hasErrored: true }));
}
  • Then we’ll call it when the component mounts:
componentDidMount() {
this.fetchData('http://5826ed963900d612000138bd.mockapi.io/items');
}

Which leaves us with (unchanged lines omitted):

class ItemList extends Component {
constructor() {
this.state = {
items: [],
};
}
    fetchData(url) {
this.setState({ isLoading: true });
        fetch(url)
.then((response) => {
if (!response.ok) {
throw Error(response.statusText);
}
                this.setState({ isLoading: false });
                return response;
})
.then((response) => response.json())
.then((items) => this.setState({ items }))
.catch(() => this.setState({ hasErrored: true }));
}
    componentDidMount() {
this.fetchData('http://5826ed963900d612000138bd.mockapi.io/items');
}
    render() {
}
}

And that’s it. Your component now fetches the items from a REST endpoint! You should hopefully see "Loading…"appear briefly before the 4 list items. If you pass in a broken URL to fetchData you should see our error message.

However, in reality, a component shouldn’t include logic to fetch data, and data shouldn’t be stored in a component’s state, so this is where Redux comes in.

Converting to Redux

To start, we need to add Redux, React Redux and Redux Thunk as dependencies of our project so we can use them. We can do that with:

npm install redux react-redux redux-thunk --save

Understanding Redux

There are a few core principles to Redux which we need to understand:

  1. There is 1 global state object that manages the state for your entire application. In this example, it will behave identically to our initial component’s state. It is the single source of truth.
  2. The only way to modify the state is through emitting an action, which is an object that describes what should change. Action Creators are the functions that are dispatched to emit a change – all they do is return an action.
  3. When an action is dispatched, a Reducer is the function that actually changes the state appropriate to that action – or returns the existing state if the action is not applicable to that reducer.
  4. Reducers are “pure functions”. They should not have any side-effects nor mutate the state — they must return a modified copy.
  5. Individual reducers are combined into a single rootReducer to create the discrete properties of the state.
  6. The Store is the thing that brings it all together: it represents the state by using the rootReducer, any middleware (Thunk in our case), and allows you to actually dispatch actions.
  7. For using Redux in React, the <Provider /> component wraps the entire application and passes the storedown to all children.

This should all become clearer as we start to convert our application to use Redux.

Designing our state

From the work we’ve already done, we know that our state needs to have 3 properties: items, hasErrored and isLoading for this application to work as expected under all circumstances, which correlates to needing 3 unique actions.

Now, here is why Action Creators are different to Actions and do not necessarily have a 1:1 relationship: we need a fourth action creator that calls our 3 other action (creators) depending on the status of fetching the data. This fourth action creator is almost identical to our original fetchData() method, but instead of directly setting the state with this.setState({ isLoading: true }), we'll dispatch an action to do the same: dispatch(isLoading(true)).

Creating our actions

Let’s create an actions/items.js file to contain our action creators. We'll start with our 3 simple actions.

export function itemsHasErrored(bool) {
return {
type: 'ITEMS_HAS_ERRORED',
hasErrored: bool
};
}
export function itemsIsLoading(bool) {
return {
type: 'ITEMS_IS_LOADING',
isLoading: bool
};
}
export function itemsFetchDataSuccess(items) {
return {
type: 'ITEMS_FETCH_DATA_SUCCESS',
items
};
}

As mentioned before, action creators are functions that return an action. We export each one so we can use them elsewhere in our codebase.

The first 2 action creators take a bool (true/false) as their argument and return an object with a meaningful type and the bool assigned to the appropriate property.

The third, itemsFetchDataSuccess(), will be called when the data has been successfully fetched, with the data passed to it as items. Through the magic of ES6 property value shorthands, we'll return an object with a property called items whose value will be the array of items;

Note: that the value you use for type and the name of the other property that is returned is important, because you will re-use them in your reducers

Now that we have the 3 actions which will represent our state, we’ll convert our original component’s fetchDatamethod to an itemsFetchData() action creator.

By default, Redux action creators don’t support asynchronous actions like fetching data, so here’s where we utilise Redux Thunk. Thunk allows you to write action creators that return a function instead of an action. The inner function can receive the store methods dispatch and getState as parameters, but we'll just use dispatch.

A real simple example would be to manually trigger itemsHasErrored() after 5 seconds.

export function errorAfterFiveSeconds() {
// We return a function instead of an action object
return (dispatch) => {
setTimeout(() => {
// This function is able to dispatch other action creators
dispatch(itemsHasErrored(true));
}, 5000);
};
}

Now we know what a thunk is, we can write itemsFetchData().

export function itemsFetchData(url) {
return (dispatch) => {
dispatch(itemsIsLoading(true));
        fetch(url)
.then((response) => {
if (!response.ok) {
throw Error(response.statusText);
}
                dispatch(itemsIsLoading(false));
                return response;
})
.then((response) => response.json())
.then((items) => dispatch(itemsFetchDataSuccess(items)))
.catch(() => dispatch(itemsHasErrored(true)));
};
}

Creating our reducers

With our action creators defined, we now write reducers that take these actions and return a new state of our application.

Note: In Redux, all reducers get called regardless of the action, so inside each one you have to return the original state if the action is not applicable.

Each reducer takes 2 parameters: the previous state (state) and an action object. We can also use an ES6 feature called default parameters to set the default initial state.

Inside each reducer, we use a switch statement to determine when an action.type matches. While it may seem unnecessary in these simple reducers, your reducers could theoretically have a lot of conditions, so if/else if/else will get messy fast.

If the action.type matches, then we return the relevant property of action. As mentioned earlier, the type and action[propertyName] is what was defined in your action creators.

OK, knowing this, let’s create our items reducers in reducers/items.js.

export function itemsHasErrored(state = false, action) {
switch (action.type) {
case 'ITEMS_HAS_ERRORED':
return action.hasErrored;
        default:
return state;
}
}
export function itemsIsLoading(state = false, action) {
switch (action.type) {
case 'ITEMS_IS_LOADING':
return action.isLoading;
        default:
return state;
}
}
export function items(state = [], action) {
switch (action.type) {
case 'ITEMS_FETCH_DATA_SUCCESS':
return action.items;
        default:
return state;
}
}

Notice how each reducer is named after the resulting store’s state property, with the action.type not necessarily needing to correspond. The first 2 reducers hopefully make complete sense, but the last, items(), is slightly different.

This is because it could have multiple conditions which would always return an array of items: it could return all in the case of a fetch success, it could return a subset of items after a delete action is dispatched, or it could return an empty array if everything is deleted.

To re-iterate, every reducer will return a discrete property of the state, regardless of how many conditions are inside that reducer. That initially took me a while to get my head around.

With the individual reducers created, we need to combine them into a rootReducer to create a single object.

Create a new file at reducers/index.js.

import { combineReducers } from 'redux';
import { items, itemsHasErrored, itemsIsLoading } from './items';
export default combineReducers({
items,
itemsHasErrored,
itemsIsLoading
});

We import each of the reducers from items.js and export them with Redux's combineReducers(). As our reducer names are identical to what we want to use for a store's property names, we can use the ES6 shorthand.

Notice how I intentionally prefixed my reducer names, so that when the application grows in complexity, I’m not constrained by having a “global” hasErrored or isLoading property. You may have many different features that could error or be in a loading state, so prefixing the imports and then exporting those will give your application's state greater granularity and flexibility. For example:

import { combineReducers } from 'redux';
import { items, itemsHasErrored, itemsIsLoading } from './items';
import { posts, postsHasErrored, postsIsLoading } from './posts';
export default combineReducers({
items,
itemsHasErrored,
itemsIsLoading,
posts,
postsHasErrored,
postsIsLoading
});

Alternatively, you could alias the methods on import, but I prefer consistency across the board.

Configure the store and provide it to your app

This is pretty straightforward. Let’s create store/configureStore.js with:

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from '../reducers';
export default function configureStore(initialState) {
return createStore(
rootReducer,
initialState,
applyMiddleware(thunk)
);
}

Now change our app’s index.js to include <Provider />, configureStore, set up our store and wrap our app (<ItemList />) to pass the store down as props:

import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import configureStore from './store/configureStore';
import ItemList from './components/ItemList';
const store = configureStore(); // You can also pass in an initialState here
render(
<Provider store={store}>
<ItemList />
</Provider>,
document.getElementById('app')
);

I know, it’s taken quite a bit of effort to get to this stage, but with the set up complete, we can now modify our component to make use of what we’ve done.

Converting our component to use the Redux store and methods

Let’s jump back in to components/ItemList.js.

At the top of the file, import what we need:

import { connect } from 'react-redux';
import { itemsFetchData } from '../actions/items';

connect is what allows us to connect a component to Redux's store, and itemsFetchData is the action creator we wrote earlier. We only need to import this one action creator, as it handles dispatching the other actions.

After our component’s class definition, we're going to map Redux's state and the dispatching of our action creator to props.

We create a function that accepts state and then returns an object of props. In a simple component like this, I remove the prefixing for the has/is props as it’s obvious that they're related to items.

const mapStateToProps = (state) => {
return {
items: state.items,
hasErrored: state.itemsHasErrored,
isLoading: state.itemsIsLoading
};
};

And then we need another function to be able to dispatch our itemsFetchData() action creator with a prop.

const mapDispatchToProps = (dispatch) => {
return {
fetchData: (url) => dispatch(itemsFetchData(url))
};
};

Again, I’ve removed the items prefix from the returned object property. Here fetchData is a function that accepts a url parameter and returns dispatching itemsFetchData(url).

Now, these 2 mapStateToProps() and mapDispatchToProps() don't do anything yet, so we need to change our final export line to:

export default connect(mapStateToProps, mapDispatchToProps)(ItemList);

This connects our ItemList to Redux while mapping the props for us to use.

The final step is to convert our component to use props instead of state, and to remove the leftovers.

  • Delete the constructor() {} and fetchData() {} methods as they're unnecessary now.
  • Change this.fetchData() in componentDidMount() to this.props.fetchData().
  • Change this.state.X to this.props.X for .hasErrored.isLoading and .items.

Your component should now look like this:

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { itemsFetchData } from '../actions/items';
class ItemList extends Component {
componentDidMount() {
this.props.fetchData('http://5826ed963900d612000138bd.mockapi.io/items');
}
    render() {
if (this.props.hasErrored) {
return <p>Sorry! There was an error loading the items</p>;
}
        if (this.props.isLoading) {
return <p>Loading…</p>;
}
        return (
<ul>
{this.props.items.map((item) => (
<li key={item.id}>
{item.label}
</li>
))}
</ul>
);
}
}
const mapStateToProps = (state) => {
return {
items: state.items,
hasErrored: state.itemsHasErrored,
isLoading: state.itemsIsLoading
};
};
const mapDispatchToProps = (dispatch) => {
return {
fetchData: (url) => dispatch(itemsFetchData(url))
};
};
export default connect(mapStateToProps, mapDispatchToProps)(ItemList);

And that’s it! The application now uses Redux and Redux Thunk to fetch and display the data!

That wasn’t too difficult, was it?

And you’re now a Redux master :D

What next?

I’ve put all of this code up on GitHub, with commits for each step. I want you to clone it, run it and understand it, then add the ability for the user to delete individual list items based on the item’s index.

I haven’t yet really mentioned that in Redux, the state is immutable, which means you can’t modify it, so have to return a new state in your reducers instead. The 3 reducers we wrote above were simple and “just worked”, but deleting items from an array requires an approach that you may not be familiar with.

You can no longer use Array.prototype.splice() to remove items from an array, as that will mutate the original array. Dan explains how to remove an element from an array in this video, but if you get stuck, you can check out (pun intended) the delete-items branch for the solution.

I really hope that this has clarified the concept of Redux and Thunk and how you might go about converting an existing React application to use them. I know that writing this has solidified my understanding of it, so I’m very happy to have done it.

I’d still recommend reading the Redux docs, watching Dan’s videos, and re-doing Wes’ course as you should hopefully now be able to understand some of the other more complex and deeper principles.

This article has been cross-posted on Codepen for better code formatting.

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.