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. 😊
Let's have a look at the bigger picture first and then dig deeper.
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 😁...
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!
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. 🤓
We will have two sets of action 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
}
};
}
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 🔥...
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! 😇
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! 😎
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! 🙌
Enjoyed the article? Share the summary thread on twitter.