Async Loading with Redux Thunk

In this article we're going to continue our look into loading Articles using Redux. We've done the initial setup. Now we're going to use asynchronous actions with a new library, Redux Thunk, to pull the markdown data from an API.

We're going to start where we left off in the last article.
If you need to catch up:

git clone https://github.com/lawwantsin/webpack-course.git
cd webpack-course
git checkout redux-thunk
npm install

Missing Image

Redux Thunk

Let's jump right in. In the terminal let's install Redux Thunk as well as a fetch polyfill that will work on the client as well as the server:

npm i redux-thunk cross-fetch

Fetching Reducer

Now let's setup our new action, we'll call it fetchArticle and it will be unlike our previous action in that it will return a function. That function contains the dispatch and the getState functions from the store. So this is a way to pass the store to your action for further dispatch and use of the store state.

In actions.js:

import fetch from "cross-fetch"

export const fetchArticle = (site, slug) => dispatch => {
  if (!site || !slug) return
  fetch(`https://${site}.local/api/articles/${slug}`)
    .then(res => res.json())
    .then(items => dispatch(fetchSuccess(items)))
    .catch(err => dispatch(fetchError(err)))
}

These kinds of actions don't act normally when you return an action. That won't work and the action will never hit the reducer. Instead we dispatch other actions, that are just plain objects. So we'll add some of those as well. Let's create the actions and corresponding reducer for the fetching.

In actions.js:

export const FETCH_SUCCESS = "FETCH_SUCCESS"
export const fetchSuccess = response => {
  return {
    type: FETCH_SUCCESS,
    payload: response
  }
}

export const FETCH_FAILURE = "FETCH_FAILURE"
export const fetchFailure = error => {
  return {
    type: FETCH_FAILURE,
    payload: error
  }
}

export const FETCH_ERROR = "FETCH_ERROR"
export const onError = error => {
  return {
    type: FETCH_ERROR,
    payload: error
  }
}

In reducers.js:

import { FETCH_SUCCESS, FETCH_FAILURE, FETCH_ERROR } from "./actions"

export const fetchArticle = (state = {}, action) => {
  switch (action.type) {
    case FETCH_SUCCESS:
      return {
        ...state,
        content: action.payload
      }
    case FETCH_FAILURE:
      return {
        ...state,
        response: action.payload
      }
    default:
      return state
  }
}

Now in store, let's add our new fetchArticle reducer.

In store.js:

import { createStore, applyMiddleware, compose } from "redux"
import { fetchArticle } from "./reducers"
import thunk from "redux-thunk"

const composeEnhancers =
  typeof window === "object" && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
    ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({})
    : compose

const enhancer = composeEnhancers(applyMiddleware(thunk))

export default createStore(fetchArticle, enhancer)

Okay, so now that we've got the Redux code down, let's setup the endpoint. This is a simple MVP that just returns the post parameter in JSON form.

In express.js:

server.get("/api/articles/:post", (req, res) => {
  res.json(req.params.post)
})

Now let's use the props we've been using already to dispatch this fetchArticle action.

In Article.js:

componentDidMount() {
  this.props.dispatch(fetchArticle(this.props.site, this.props.match.params.slug))
}
...
export default connect(state => ({
  content: state.content
}))(Article)

In Sum

Okay, so we've done a great deal here. We've added Redux thunk to our store to use asynchronous actions to pull the data from our server endpoint. We also added hot reloading of the reducers in development.

If you need the final code:

git checkout redux-thunk-final

Up Next

Next we'll be completing this API endpoint to serve the real markdown data back to our client. We've got a good test setup, so we know it's working. It's time to finish the job. See you there.