Skip to main content

React-Redux: Asynchronous Actions (Redux-Thunk)

Intro

This post will cover how to fetch data from an API inside of a React app using Redux and Redux-Thunk. In this example we are going to be creating a simple app that displays a list of blog posts that have been fetched from an API.

General Data Loading With Redux

General Process

This is the process that we will be following.

  • Component gets rendered onto the screen
  • Component’s ‘componentDidMount’ lifecycle method gets called
  • We call action creator from ‘componentDidMount’
  • Action creator runs code to make an API request
  • API responds with data
  • Action creator returns an ‘action’ with the fetched data on the ‘payload’ property.
  • Some reducer sees the action, returns the data off the ‘payload’
  • Because we generated some new state object, redux/react-redux cause our React app to be re-rendered.

Separation of Concerns

general data loading with redux diagram

App Scaffolding

In this section we are just going to cover the scaffolding of the app. You can skip this if you want to go straight to the thunk. First we generate a new app with actions and reducers folders and indexes. First we need an action.

Creating Actions

First we can scaffold out a basic action that doesn’t even have a payload at this point, we just need to get this wired up.

export const fetchPosts = () => {
return {
type: 'FETCH_POSTS'
};
};

Connect Component to Action

Here is our basic PostList component. We make sure to import connect and the fetchPosts action. We create a class component. Then at the bottom we wire up connect. We don’t have a mapStateToProps function yet, so we just put null for that. For the second argument we pass in our action that we created, fetchPosts. And lastly we pass the component into the second function as always.

This connect function is telling our Provider that this component is going to use this action. Once our reducers are complete we can come back and add the mapStateToProps function.

import React from 'react';
import { connect } from 'react-redux';
import { fetchPosts } from '../actions';

class PostList extends React.Component {
render() {
return <div>Post List</div>;
}
}
export default connect(null, {fetchPosts})(PostList);

Component Mounts and Calls Action

At this point we can add the componentDidMount function (which is a default React method that fires as soon as the component has rendered) and inside of that we run our fetchPosts action that is now being passed down as a prop from the store.

import React from 'react';
import { connect } from 'react-redux';
import { fetchPosts } from '../actions';

class PostList extends React.Component {
componentDidMount() {
this.props.fetchPosts();
}

render() {
return <div>Post List</div>;
}
}
export default connect(null, {fetchPosts})(PostList);

So at this point we have an action (which does nothing) that fires as soon as the PostList component renders.

general data loading with redux diagram updated

API Request Scaffolding

We will be using Axios to facilitate our API requests. A bit of file structure, we make a new folder in src called “apis” and then inside that we make a file called jsonPlaceholder.js. Thus named because we are going to be using the jsonplaceholder.typicode.com to get dummy data for this application. Then following our typical axios workflow we place the following in our api config file.

import axios from 'axios';

export default axios.create({
baseURL:'https://jsonplaceholder.typicode.com'
});

Typically we would also place an API key into this file, but this service does not require one.

API Request In Action Creator

Let’s start by looking at a bad approach to this problem.

import jsonPlaceholder from '../apis/jsonPlaceholder';

export const fetchPosts = async () => {
// Bad Approach!!!
const response = await jsonPlaceholder.get('/posts');

return {
type: 'FETCH_POSTS',
payload: response
};
};

This is a bad approach because it breaks one of the rules of Redux Actions.

error actions must be plain objects. use custom middleware for async actions

As the error message implies, we cannot use async function for Redux Actions. This is where the Thunk Middleware comes into play.

Understanding Async Action Creators

Why are Async function in action creators verboten? The answer is not intuitive.

Problem 1

It is because Redux is expecting a plain object to be returned, and it LOOKS like that is what is happening. But in reality, once the code has been transpiled through Babel it initially returns a request object, and not a plain object, which is what causes the error.

Problem 2

The second problem is common with API requests across platforms. It takes time. If we cannot make the action dispatch wait until we have gotten the API request back with an asynchronous function, then the reducers on the action will run before the data they are supposed to be working with has arrived.

All of this could be avoided if React did not take care to be compatible with a wide range of browsers by transpiling everything with Babel. But we don’t live in that world. We live in a world where Internet Explorer ruins everything.

Middleware In Redux

Synchronous Action Creator – Instantly returns an action with data ready to go

Asynchronous Action Creator – Takes some amount of time to it to get its data ready to go

We are working towards creating an Asynchronous Action Creator. To do this we need to use a middleware like redux-thunk. In redux a middleware exists between dispatch and the reducers. What exactly is middleware (in this context)?

  • A function that gets called with every action we dispatch
  • Has the ability to STOP, MODIFY, or otherwise mess around with actions
  • Tons of open source middleware exists
  • Most popular use of middleware is for dealing with async actions
  • We are going to use a middleware called ‘Redux-Thunk’ to solve our async issues

Normal Action Creators vs Thunk

Normal action creators have the following properties.

  • Action Creators must return action objects
  • Actions must have a type property
  • Actions can optionally have a ‘payload’

Action creators with Redux-Thunk have the following properties.

  • Action Creators can return objects or functions
  • If an action object gets returned, it must have a type
  • If an action object get returned, it can optionally have a ‘payload’

Redux-Thunk Cycle

When Redux-Thunk encounters an action object it simply tells it to “move along” and passes it to the Reducer where it would normally go. If the action is a function Thunk kicks in.

redux-thunk cycle diagram

Action Creators With Thunk

Here is a look at what our action creator looks like with thunk. We can see that we are returning an Async function that takes dispatch and getState as its arguments, and then we manually fire dispatch with our action as an argument.

import jsonPlaceholder from "../apis/jsonPlaceholder";

export const fetchPosts = () => {
return async function (dispatch, getState) {
const response = await jsonPlaceholder.get("/posts");

dispatch({ type: 'FETCH_POSTS', payload: response})
};
};

Note that it is totally ok to have standard action objects in our action index also, this is just the syntax that is used anytime we need to use a function because we require async. Also, thanks to modern ES2015 syntax we can refactor this to be a bit shorter.

import jsonPlaceholder from "../apis/jsonPlaceholder";

export const fetchPosts = () => async (dispatch) => {
const response = await jsonPlaceholder.get("/posts");

dispatch({ type: "FETCH_POSTS", payload: response });
};

Because we aren’t actually using getState on this action we can remove that as a second argument. We can convert the inner function to an arrow function. Also because there is only one possible item to return, we can strip out return and move the inner function call up to the same line as the first. This is completely valid and equivalent.

Applying Middleware To A Redux Store

We do of course need to import Thunk, and we do that at our primary index level, because that is where the provider lives. Here is our src/index.js file. We have imported thunk, and we have also added the applyMiddleware method from redux.

import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import { createStore, applyMiddleware } from "redux";
import thunk from 'redux-thunk';

import App from "./components/App";
import reducers from "./reducers";

ReactDOM.render(
<Provider store={createStore(reducers)}>
<App />
</Provider>,
document.querySelector("#root")
);

and then once we have properly imported these items let’s break our store prop out of the provider into a helper constant and modify it.

import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import { createStore, applyMiddleware } from "redux";
import thunk from 'redux-thunk';

import App from "./components/App";
import reducers from "./reducers";

const store = createStore(reducers, applyMiddleware(thunk));

ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.querySelector("#root")
);

And we can see that we have simply added applyMiddleware as a second argument and passed in thunk!

Reducers

At this point we are almost done with the thunk cycle. We just need to work on our reducers and we will be done!

general data loading with redux diagram updated 2

Reducer Organization

This is a bit off topic but let’s just briefly look at a new way to organize our reducers. Like components, as the application grows we want to break our reducers out into separate files. Here is a quick example. We can start by making a new reducer in the reducer index called postsReducer.

export default () => {
return 123;
};

and then in our reducers/index file we simply add that into our combine reducers function.

import { combineReducers } from 'redux';
import postsReducer from './postsReducer';

export default combineReducers({
posts: postsReducer
});

It’s easy to see why this would be useful as the application grows!

Rules of Reducers

  • Must return any value besides ‘undefined’
  • Produces ‘state’, or data to be used inside of your app using only previous state and the action (reducers are pure)
  • Must not return reach ‘out of itself’ to decide what value to return
  • Must not mutate its input ‘state’ argument

The last rule is a bit tricky. The reason that thunk does not want you to mutate the state input, is that if it detects the same state on memory (because it was modified and not created anew) then it will assume that no changes were made and the component will not update. That’s the short version.

Safely Updating State In Reducers

safely updating state in reducers diagram

Switch Statements In Reducers

Up until now we have been using IF statements in our reducers to determine if the action type matches, and if not return the previous state.

export default (state = [], action) => {
if (action.type='FETCH_POSTS') {
return action.payload;
}
return state;
};

This is fine but there is another way that we can accomplish this. Switch Statements.

MDN: Switch

export default (state = [], action) => {
switch (action.type) {
case "FETCH_POSTS":
return action.payload;
default:
return state;
}
};

Dispatching Correct Values

Switching back to our PostList component, it’s finally time to pass our fetched information into the component as a prop. We currently have the information stored in our Redux store. Now we need to pass it in as a prop. It’s time to mapStateToProps().

import React from 'react';
import { connect } from 'react-redux';
import { fetchPosts } from '../actions';

class PostList extends React.Component {
componentDidMount() {
this.props.fetchPosts();
}

render() {
return <div>Post List</div>;
}
}

const mapStateToProps = (state) => {
return { posts: state.posts };
}

export default connect(mapStateToProps, {fetchPosts})(PostList);

Props to posts is now available to us as an array. We will need to map out the array to iterate over the posts and display them, but they are available!

console.log(this.props.posts);

post title console log

Rendering The List

This is all review at this point, but we are going to render the post array in the PostList component now.

We can start by making a helper function called renderList where we will map out the array. Then we simply insert that method into the render method.

import React from "react";
import { connect } from "react-redux";
import { fetchPosts } from "../actions";

class PostList extends React.Component {
componentDidMount() {
this.props.fetchPosts();
}

renderList() {
return this.props.posts.map((post) => {
return (
<div className="item" key={post.id}>
<i className="large middle aligned icon user" />
<div className="content">
<div className="description">
<h2>{post.title}</h2>
<p>{post.body}</p>
</div>
</div>
</div>
);
});
}

render() {
console.log(this.props.posts);
return <div className="ui relaxed divided list">{this.renderList()}</div>;
}
}

const mapStateToProps = (state) => {
return { posts: state.posts };
};

export default connect(mapStateToProps, { fetchPosts })(PostList);

And voila.

post data rendered in template

Next Steps: Fetching and Displaying Post Author

This little application has another step. We want to fetch and display all of the authors of these posts. The trick is that the API only returns an author ID, and the author data is stored in another location. This will require another set of API requests. We are going to go ahead and move this to the next post because we are getting pretty long on this one.

GitHub Repo

Ncoughlin: React-Bloglist

Comments

Recent Work

Free desktop AI Chat client, designed for developers and businesses. Unlocks advanced model settings only available in the API. Includes quality of life features like custom syntax highlighting.

Learn More

BidBear

bidbear.io

Bidbear is a report automation tool. It downloads Amazon Seller and Advertising reports, daily, to a private database. It then merges and formats the data into beautiful, on demand, exportable performance reports.

Learn More