Fetch API Data using Custom Redux Middleware

Cover image

Using Redux and need a middleware? what about creating your custom middleware to fetch data from an API? this will be easier than you think! 👌

In some projects, you may need something more advanced than redux-trunk. But a bit less complicated than redux-saga or redux-observables.

Even though redux-saga & redux-observables are powerful tools, they have a steep learning curve. Depending on your use-case. It may be an acceptable tradeoff to create your custom middleware instead.

In this tutorial, we will cover how to create your custom middleware. Using tools such as Axios and vanilla Redux.

You can use React Hooks to create the same middleware. But we will use plain vanilla React & Redux for this tutorial. 😊

Overview

Let's have a look at the bigger picture first and then dig deeper.

Project structure

I have created a small react project to show different data fetching patterns. The project was initialized create-react-app has a standard structure. 👇

├── README.md
├── package.json
├── public
│   ├── favicon.ico
│   ├── index.html
│   ├── logo192.png
│   ├── logo512.png
│   ├── manifest.json
│   └── robots.txt
├── src
│   ├── App.css
│   ├── App.js
│   ├── App.test.js
│   ├── actions
│   │   ├── api.js
│   │   ├── index.js
│   │   └── types.js
│   ├── components
│   │   ├── PostsList.js
│   │   ├── WithCustomMiddleware.js
│   ├── index.css
│   ├── index.js
│   ├── middleware
│   │   └── api.js
│   ├── reducers
│   │   └── index.js
│   ├── serviceWorker.js
│   └── store
│       └── index.js
└── yarn.lock

Let's start with the standard Redux setup 😁...

Create Store

First, create a store and add a middleware like this:

// store/index.js
import { createStore, applyMiddleware } from 'redux';
import rootReducer from '../reducers';
import apiMiddleware from '../middleware/api';

const store = createStore(rootReducer, applyMiddleware(apiMiddleware));
window.store = store;
export default store;

Up next, providers!

Provide Store

We need to provide the store to the root App component.

// src/index.js

import { Provider } from 'react-redux';
import store from './store';

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

Now it's time to dig a bit deeper, let's define the store's actions. 🤓

Actions

We will have two sets of action types:

  • API types
  • posts (some data from an API) types.

Here is how we will define these...

// actions/types.js
export const FETCH_POSTS = 'FETCH_POSTS';
export const SET_POSTS = 'SET_POSTS';

export const API = 'API';
export const API_START = 'API_START';
export const API_END = 'API_END';
export const API_ERROR = 'API_ERROR';

Next, let's define the actual actions like this...

// actions/index.js
import { FETCH_POSTS, SET_POSTS, API } from './types';

export function fetchPosts() {
  return apiAction({
    url: 'https://jsonplaceholder.typicode.com/posts', // Mocked Backend Data.
    onSuccess: setPosts,
    onFailure: () => console.log('Error while loading posts'), // Dummy error handler.
    label: FETCH_POSTS
  });
}

export function setPosts(data) {
  return {
    type: SET_POSTS,
    payload: data
  };
}

function apiAction({
  url,
  method = 'GET', // Default method
  data = null,
  onSuccess = () => {},
  onFailure = () => {},
  label
}) {
  return {
    type: API,
    payload: {
      url,
      method,
      data,
      onSuccess,
      onFailure,
      label
    }
  };
}
  • apiAction makes all backend server calls.
  • fetchPosts calls the apiAction action to make a request.
  • setPosts sets the data returned from the server to the store.

Last step for actions: API specific actions...

// actions/api.js
import { API_START, API_END, API_ERROR } from './types';

export const apiStart = label => ({
  type: API_START,
  payload: label
});

export const apiEnd = label => ({
  type: API_END,
  payload: label
});

export const apiError = error => ({
  type: API_ERROR,
  error
});

These are simple actions to allow us to update our store's state when an api call starts, ends or fails.

Now let's have a look at the reducers 🔥...

Reducers

Inside an index.js file we will handle three actions:

// reducers/index.js
import { SET_POSTS, API_START, API_END, FETCH_POSTS } from '../actions/types';

export default function(state = {}, action) {
  switch (action.type) {
    case SET_POSTS:
      return { data: action.payload };
    case API_START:
      if (action.payload === FETCH_POSTS) {
        return {
          ...state,
          isFetching: true // Shows loading state while fetching data from backend.
        };
      }
      break;
    case API_END:
      if (action.payload === FETCH_POSTS) {
        return {
          ...state,
          isFetching: false 
        };
      }
      break;
    default:
      return state;
  }
}

Sweet... now its time to create the middleware. I promise this one will be easy! 😇

Middleware

We will make API requests using Axios. Through the middleware, we can add generic Axios configurations such as a BASE URL.

Here is a simple version of how the middleware may look like:

// middleware/index.js
import axios from 'axios';
import { API } from '../actions/types';
import { apiEnd, apiStart, apiError } from '../actions/api';

const apiMiddleware = ({ dispatch }) => next => action => {
  next(action);

  if (action.type !== API) { // only apply middleware to actions of type API
    return;
  }

  const { url, method, data, onSuccess, onFailure, label } = action.payload;

  // Adds support to POST and PUT requests with data 
  const dataOrParams = ['GET', 'DELETE'].includes(method) ? 'params' : 'data'; 

  // axios configs
  axios.defaults.baseURL = process.env.REACT_APP_BASE_URL || '';
  axios.defaults.headers.common['Content-Type'] = 'application/json';

  if (label) {
    dispatch(apiStart(label)); // Action to notify that the api call is starting.
  }

  axios
    .request({
      url,
      method,
      [dataOrParams]: data
    })
    .then(({ data }) => {
      dispatch(onSuccess(data));
    })
    .catch(error => {
      dispatch(apiError(error));
       // Original apiAction executor's error handler. e.g. Fn passed inside fetchPosts action. 
      dispatch(onFailure(error));
    })
    .finally(() => {
      if (label) {
         // Action to notify that the api call has ended.
        dispatch(apiEnd(label));
      }
    });
};

export default apiMiddleware;

Now you have a starting point, you can build on top of this foundation depending on what you need.

For instance, you can add special logic to make requests with authentication tokens. Another logic could be including special HTTP headers.

Let's see the middleware in action! 😎

Redux Custom Middleware

Example:

// components/WithCustomMiddleware.js

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

class WithCustomMiddleware extends Component {
  state = {};

  componentDidMount() {
    this.props.fetchPosts();
  }

  render = () => (
    <PostsList
      data={this.props.data}
      isFetching={this.props.isFetching}
    ></PostsList>
  );
}

const mapStateToProps = ({ data = [], isFetching = false }) => ({
  data,
  isFetching
});

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

Note: - As you can see, this middleware isn't very sophisticated. Think about your use-cases if:

  • You have to deal with advanced issues.
  • You don't need the flexibility of a custom middleware.

It might be easier to use redux-saga or redux-observables, it all depends on your use-case.

Now you know your way around creating your own Redux custom API middleware! 🙌

Support us

Enjoyed the article? Share the summary thread on twitter.



Author image

Learn how to build scalable, fast and accessible web applications.

Follow us on Twitter

hello@nordschool.com