React + Redux Tutorial Part III: Async Redux

Async Redux: Connecting React to an External API


This is Part III of a eight part series on building a CRUD application with React + Redux. You can view the code for this project here. You can view the table of contents here


In order for our CatsPage component to display all the wonderful cats, our React app needs to make an API request, receive the cats payload, and somehow pass that data down to the CatsPage component.

This is where our store, action creators and reducers will come in. Let's break down this flow before we start building.

We'll walk through this flow using the example of retrieving all the cats from the API.

  1. Store will dispatch an action that makes a request to http://my-cat-api.com/api/v1/cats.
  2. The action that makes this API request will wait to get a response back from the API. When that response comes back with the payload of all the cats, it will dispatch a new action that contains the cats payload.
  3. The store catches the action and forwards it to the reducer. The reducer will create a new, updated, copy of state, with all of the cats from the cats payload contained in it.
  4. Changes to state get emitted to any components that are connected to the store. This will trigger those components to first update their properties using the fresh, updated copy of state created by the reducer, and then re-render with that new data.

Now that we understand the basic flow, let's start building!

Configuring The Store

We'll configure our store in src/store/configureStore.js.

import {createStore, applyMiddleware} from 'redux';
import rootReducer from '../reducers/rootReducer';
import thunk from 'redux-thunk';

export default function configureStore() {
  return createStore(
    rootReducer,
    applyMiddleware(thunk)
  );
}

Our invocation of the createStore function does two things:

  • Connect our store to the rootReducer, which we'll define soon. Our root reducer will wrap up our individual reducers. It's a good practice to build a few different reducers to handle different sets of actions. For example, since we have cat data and hobby data, we'll build a cat reducer and a hobby reducer. While we could gave one giant reducer that handles all of our actions, we'll choose to be a bit more organized about it.
  • Utilize the Thunk middleware, which will allow us to construct our action creators in a very special way that we'll discuss soon.

Now that our store is born, we want to make sure that any parent, or container components, we build will have access to it.

For example, we know that our CatsPage component needs access to the store so that it can receive the cats payload and display all the amazing cats.

We'll use the Redux Provider to make the store available to any components that we choose to connect to it.

Passing the Store with Redux Provider

Redux Provider is actually a component that Redux gives us access to. We'll wrap our <Router> component from our application's entry point, src/index.js, in a <Provider> component, and pass in an instance of our store.

// src/index.js

/*eslint-disable import/default */
import 'babel-polyfill';
import React from 'react';
import { render } from 'react-dom';
import configureStore from './store/configureStore';
import { Provider } from 'react-redux';
import { Router, browserHistory } from 'react-router';
import routes from './routes';

const store = configureStore();

render(
  <Provider store={store}>
    <Router history={browserHistory} routes={routes} />
  </Provider>,
  document.getElementById('app')
);

Okay, we're ready to build out the ability for our app to retrieve all the cats from the API.

Actions and Action Creators

We know we need to make a web request to the /cats API end point. But where should we make that request?

With Redux and Thunk, we can implement a pattern in which some action creators are, as you would expect, simple JSON objects with a key of type, designating the name or type of the action that is dispatched, and other, optional, keys pointing to any payload we may need to update state. Other action creators, however, with the help of Thunk, as actually functions that can make asynchronous web requests and invoke a dispatch function to dispatch additional actions.

Without Thunk, or something like it, the dispatch function is only available to be called on our store instance.

So, we'll build out two action creators to start with. One that makes our API call and uses thunk to dispatch the other action, which will send our API payload to the reducer, via the store.

The loadCats Action Creator

We'll place our cat-related actions in a file src/actions/catActions.js.

Let's start by defining our loadCats action creator function.

// src/actions/catActions.js

import catApi from '../api/catApi';

export function loadCats() {
  return function(dispatch) {
    return catApi.getAllCats().then(cats => {
      dispatch(loadCatsSuccess(cats));
    }).catch(error => {
      throw(error);
    });
  };
}

Thanks to Thunk, when we dispatch an action, we will have access to dispatch as an argument. Thunk will also absorb the dispatch of the loadCats function so that it doesn't end up getting thrown to the reducers. Our reducers will only receive the normal object actions.

So, our loadCats function calls on our catApi instance (we'll come back to that in a moment), and then dispatches another action, loadCatsSuccess, with an argument of the cats payload we received from the API.

Let's take a step back before we move forward with this second, loadCatsSuccess action.

Invoking the loadCats function

At what point in time do we want to load all the cats from the API? Since the whole point of our app is to allow users to browse cats, we want to load up those cats for them right away. So, we'll have our store dispatch the loadCats function in our index.js file.

// src/index.js

/*eslint-disable import/default */
import 'babel-polyfill';
import React from 'react';
import { render } from 'react-dom';
import configureStore from './store/configureStore';
import { Provider } from 'react-redux';
import { Router, browserHistory } from 'react-router';
import routes from './routes';
import {loadCats} from './actions/catActions';
  
const store = configureStore();

store.dispatch(loadCats());

render(
  <Provider store={store}>
    <Router history={browserHistory} routes={routes} />
  </Provider>,
  document.getElementById('app')
);

Now we have the store dispatching our loadCats function, which will make our API call, and dispatch another function in turn.

One more think to take a look at before we finish building out that second action creator and define a reducer to catch it--the catApi module.

API Modules

Instead of junking up my action creator with a lot of code related to making API calls, I chose to abstract away API interactions into a helper module, catAPI.

Here, we'll define a series of functions that use fetch to interact with our API. For now, we'll start with a loadCats function, although later we'll add additional functions to handle all the necessary CRUD-related API interactions.

In src, create a new directory, api, and a new file, catApi

class CatApi {
  static getAllCats() {
    return fetch('http://localhost:5000/api/v1/cats').then(response => {
      return response.json();
    }).catch(error => {
      return error;
    });
  }

export default CatApi;

Okay, we're ready to move on to our loadCatsSuccess action.

The loadCatsSuccess Action Creator

This action creator is more straightforward. It simple returns an object that specifies the type of action and includes the cats payload.

// src/actions/catActions.js

...
export function loadCatsSuccess(cats) {
  return {type: 'LOAD_CATS_SUCCESS', cats};
}

Notice that we are using a string, 'LOAD_CATS_SUCCESS', as the value of our type property. This isn't very DRY. We will need to refer to this action type in other locations in our code, and if we decide to change the name later, we'll have to track down all of those references.

Instead, we'll use constants to store our action types.

Create a file, src/actions/actionTypes.js. We'll export our first constant:

export const LOAD_CATS_SUCCESS = 'LOAD_CATS_SUCCESS';

We'll import our constants in our catActions.js file, and change the way we refer to type in our loadCatsSuccess action creator:

// src/actions/catActions.js

import * as types from './actionTypes';
import catApi from '../api/catApi';

export function loadCatsSuccess(cats) {
  return {type: types.LOAD_CATS_SUCCESS, cats};
}

...

Now that our second action creator is up and running, let's define a reducer that will be able to handle receiving it.

Defining The Reducers

Earlier, we configured out store to use rootReducer. Now, we'll define that reducer, and use it to wrap up first a cats reducer and later a hobbies reducer as well.

The Root Reducer

We'll use Redux's combineReducers function. This is a helper function that turns an object whose values are different reducing functions into a single reducing function you can pass to createStore.

// src/reducers/rootReducer.js

import {combineReducers} from 'redux';
import cats from './catReducer';

const rootReducer = combineReducers({
  // short hand property names
  cats
})

export default rootReducer;

Our root reducer will use the cat reducer (which we'll define next), and I'm using short hand property names to include it. (cats, instead of cats: cats).

The Cat Reducer

Our cat reducer will be responsible for handling any cat-related actions, such as our loadCatsSuccess action.

// src/reducers/catReducer.js

import * as types from '../actions/actionTypes';
import initialState from './initialState';

export default function catReducer(state = initialState.cats, action) {
  switch(action.type) {
    case types.LOAD_CATS_SUCCESS:
      return action.cats
    default: 
      return state;
  }
}

Our reducer contains a switch statement, and eventually we'll build it out to include cases for each of our cat actions. For now, it only knows how to respond to the LOAD_CATS_SUCCESS action.

Reducers return application state. But, application state should be treated as immutable, so that we don't make changes to state that inadvertently affect other areas of our application, such as components subscribed to the store.

So, our reducer should return a brand-new object, with copies of any objects it needs from the previous state, and never alter the previous state.

When we are loading cats from the API, we want to completely overwrite the previous cat collection from our state with the new cat collection from our API. So no need to create any object copies here. We can simply return action.cats to return a new state.

Setting Initial State

Notice that our cat reducer sets state to a default argument of something called initialState.

We want to make sure that our application state has some initial values to work with. So, we'll build out initialState in src/reducers/initialState.js.

export default {
  cats: [],
  hobbies: []
}

Our initialState is simple, it sets a cats and a hobbies property equal to an empty array.

Putting It All Together

Okay, our Async flow is up and running. We're ready to define our CatsPage component and use it render all the cats. But first, let's recap the manner in which our app retrieves those cats and makes them available to the component.

  1. The store dispatches the loadCats action which sends an API request.
  2. When that action receives a successful response from the API, dispatch the loadCatsSuccess action to the store.
  3. The store forwards that action to the reducers
  4. The cat reducer knows how to handle that action. It responds by creating new copy of state that includes all the cats.
  5. This new state becomes available to any component subscribed to the store.

Okay, we're ready to move on to Part IV: The Index Feature >>

subscribe and never miss a post!

Blog Logo

Sophie DeBenedetto

comments powered by Disqus
comments powered by Disqus