Author, Understanding Redux. I Love God. I Love GF a little too much 💕🤣 http://thereduxjsbooks.com

Redux is a great tool that solves one of the main problems of UI frameworks: state management.

State management on the client side can quickly grow into a nightmare, and the unidirectional flow of data Redux enforces makes it easy to understand how events alter the state of your application.

Great!

Sadly, state management is just one of the many issues you have to deal with while building robust applications. What about handling side effects (like network requests, the most common)?

Redux, by itself, doesn’t provide a solution out of the box. Fortunately, the community has a good number of libraries maintained to solve the problem.

Now, just which of these is right for your project?

The truth is that each of these solutions were built with different approaches, use cases, and mental models in mind.

They all have their pros and cons.

I don’t intend to discuss all of the possible approaches, but let’s have a look at some of the most common patterns with a simple application.

A fake Medium, huh?

Take a look at the application screenshot above. It’s arguably very simple. There’s a bunch of text and a medium clap icon to the left. You can grab the GitHub repo for this app.

Note that the Medium clap is clickable. Here’s how I built the Medium clap clone in case that interests you.

Even for this simple application, you have to fetch data from the server. The JSON payload required for displaying the required view may look like this:

{ "numberOfRecommends": 1900, "title": "My First Fake Medium Post", "subtitle": "and why it makes no intelligible sense", "paragraphs": [ { "text": "This is supposed to be an intelligible post about something intelligible." }, { "text": "Uh, sorry there’s nothing here." }, { "text": "It’s just a fake post." }, { "text": "Love it?" }, { "text": "I bet you do!" } ] }

The structure of the app is indeed simple, with two major components: Article and Clap .

In components/Article.js , the article component is a stateless functional component that takes in title , subtitle , and paragraphs props. The rendered component looks like this:

const Article = ({ title, subtitle, paragraphs }) => { return ( <StyledArticle> <h1>{title}</h1> <h4>{subtitle}</h4> {paragraphs.map(paragraph => <p>{paragraph.text}</p>)} </StyledArticle> ); };

Here, StyledArticle is a regular div element styled via the CSS-in-JS solution styled-components .

It doesn’t matter if you’re familiar with any CSS-in-JS solutions. StyledArticle could be replaced with a div styled via good ol’ CSS.

Let’s get that over with and not begin an argument. 😂

The Medium clap component is exported within components/Clap.js . The code is slightly more involved and beyond the scope of this article. However, you can read up on how I built the Medium clap — it’s a five-minute read.

With both Clap and Article components in place, the App component just composes both components as seen in containers/App.js :

class App extends Component { state = {}; render() { return ( <StyledApp> <aside> <Clap /> </aside> <main> <Article /> </main> </StyledApp> ); } }

Again, you could replace StyledApp with a regular div and style it via CSS.

Now, to the meat of this article.

Considering the various alternative solutions to fetching data

Let’s have a look at some of the different ways you could chose to fetch data in your Redux app, and also consider their pros and cons.

The most popular options are arguably redux-thunk and redux-saga .

Ready?

redux-thunk and redux-promise

One of the most important things to remember is that every third-party library has its learning curve and potential scalability issues.

Of all the community libraries for managing side effects in Redux, those that work like redux-thunk and redux-promise are the easiest to get started with.

The premise is simple.

For redux-thunk , you write an action creator that doesn’t “create” an object but returns a function. This function gets passed the getState and dispatch functions from Redux.

Let’s have a look at how the fake medium app may utilize the redux-thunk library.

First, install the redux-thunk library:

yarn add redux-thunk

To make the library work as expected, it has to be applied as middleware.

In store/index.js :

import { createStore, applyMiddleware } from "redux"; import thunk from "redux-thunk"; const store = createStore(rootReducer, applyMiddleware(thunk));

The first line in the code block above imports the createStore and applyMiddleware functions from redux.

Line 2 imports the thunk from redux-thunk .

Line 3 creates the store but with an applied middleware.

Now, we are ready to make an actual network request.

I’ll be making use of axios library for making network requests but be free to replace that with any http client of your choice.

Initiating a network request is actually pretty easy with redux-thunk . You create an action creator like this (i.e., an action creator that returns a function):

export function fetchArticleDetails() { return function(dispatch) { return axios.get("https://api.myjson.com/bins/19dtxc") .then(({ data }) => { dispatch(setArticleDetails(data)); }); }; }

Upon mounting the App component, you dispatch this action creator:

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

And that’s it. Be sure to check the full code diff, as I am only highlighting the key lines here:

With that, the details of the article has been fetched and displayed in the app.

What exactly is wrong with this approach?

If you’re building a very small application, redux-thunk solves the problem and it’s perhaps the easiest to get along with.

However, the ease of use does come at a cost. Let’s consider three drawbacks.

1. Every action creator will have repetitive functionality for handling errors and setting headers

Here’s the action creator we wrote earlier:

export function fetchArticleDetails() { return function(dispatch) { return axios.get("https://api.myjson.com/bins/19dtxc") .then(({ data }) => { dispatch(setArticleDetails(data)); }); }; }

In most applications, you’ll need to make multiple requests and in different methods — GET , POST , etc.

Assume you had another action creator called recommendArticle . Now that may also look like this:

export function recommendArticle (id, amountOfRecommends) { return function (dispatch) { return axios.post("https://api.myjson.com/bins/19dtxc, { id, amountOfRecommends }); })

Oh, and if you wanted to fetch a user’s profile ?

export function fetchUserProfile() { return function(dispatch) { return axios.get("https://api.myjson.com/bins/19dtxc") .then(({ data }) => { dispatch(setUserProfile(data)); }); }; }

It doesn’t take long to see that there’s a lot of repeated functionality. And if you wanted to catch errors, you’d add a catch block to every action creator?

2. With more async action creators, testing gets harder

Async stuff is generally harder to test. Not impossible or difficult to test, it just makes it considerably harder to test.

Keeping action creators as stateless as possible, and making them simple functions makes them easier to debug and test.

With more action creators you include in your application, testing gets harder.

3. Changing the server communication strategy gets even harder.

What if a new senior developer came around and decided the team had to move from axios to another http client, say, superagent . Now, you’d have to go change it the different (multiple) action creators.

Not so easy, is it?

redux-saga and redux-observable

These are slightly more complicated than redux-thunk or redux-promise .

redux-saga and redux-observable definitely scale better, but they require a learning curve. Concepts like sagas and RxJS have to be learned, and depending on how much experience the engineers working on the team have, this may be a challenge.

So, if redux-thunk and redux-promise are too simple for your project, and redux-saga and redux-observable will introduce a layer of complexity you want to abstract from your team, where do you turn?

Custom middleware!

Most solutions like redux-thunk , redux-promise , and redux-saga use a middleware under the hood anyway. Why can’t you create yours?

Did you just say, “Why reinvent the wheel?”

Is this the perfect solution?

While reinventing the wheel does sound outrightly like a bad thing, give it a chance.

A lot of companies already build custom solutions to fit their needs anyway. In fact, that’s how a lot of open source projects began.

So, what would you expect from this custom solution ?

A centralized solution, i.e., in one module. Can handle various http methods: GET , POST , DELETE , and PUT Can handle setting custom headers Supports custom error handling, e.g., to be sent to some external logging service, or for handling authorisation errors. Allows for onSuccess and onFailure callbacks Supports labels for handling loading states

Again, depending on your specific needs, you may have a larger list.

Now, let me walk you through a decent starting point. One you can adapt for your specific use case.

A Redux middleware always begins like this:

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

And, here’s the full-fledged code for the custom API middleware. It may look like a lot at first, but I’ll explain every line shortly.

Here you go:

import axios from "axios"; import { API } from "../actions/types"; import { accessDenied, apiError, apiStart, apiEnd } from "../actions/api"; const apiMiddleware = ({ dispatch }) => next => action => { next(action); if (action.type !== API) return; const { url, method, data, accessToken, onSuccess, onFailure, label, headers } = action.payload; const dataOrParams = ["GET", "DELETE"].includes(method) ? "params" : "data"; // axios default configs axios.defaults.baseURL = process.env.REACT_APP_BASE_URL || ""; axios.defaults.headers.common["Content-Type"]="application/json"; axios.defaults.headers.common["Authorization"] = `Bearer${token}`; if (label) { dispatch(apiStart(label)); } axios .request({ url, method, headers, [dataOrParams]: data }) .then(({ data }) => { dispatch(onSuccess(data)); }) .catch(error => { dispatch(apiError(error)); dispatch(onFailure(error)); if (error.response && error.response.status === 403) { dispatch(accessDenied(window.location.pathname)); } }) .finally(() => { if (label) { dispatch(apiEnd(label)); } }); }; export default apiMiddleware;

With barely 100 lines of code, which you can grab from GitHub, you have a customized solution with a flow that is easy to reason about.

I promised to explain each line, so first, here’s an overview of how the middleware works:

First, you make some important imports, and you’ll get to see the usage of those very soon.

1. Set up the middleware

This is the typical setup required for a redux middleware, i.e.:

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

2. Dismiss irrelevant action types

if (action.type !== API) return;

The condition above is important to prevent any action except those of type, API from triggering a network request.

3. Extract important variables from the action payload

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

In order to make a successful request, there’s the need to extract the following from the action payload.

url represents the endpoint to be hit, method refers to the HTTP method of the request, data refers to any data to be sent to the server or query parameter (in the case of a GET or DELETE request), onSuccess and onFailure represent any action creators you’ll like to dispatch upon successful or failed request, and label refers to a string representation of the request.

You’ll see these used in a practical example shortly.

4. Handle any HTTP method

const dataOrParams = ["GET", "DELETE"].includes(method) ? "params" : "data";

Because this solution uses axios , and I think most HTTP clients work like this anyway, GET and DELETE methods use params while other methods may require sending some data to the server.

Thus, the variable dataOrParams will hold any of the values — params or data — depending on the method of the request.

If you have some experience developing on the web, this should not be strange.

5. Handle globals

// axios default configs axios.defaults.baseURL = process.env.REACT_APP_BASE_URL || ""; axios.defaults.headers.common["Content-Type"]="application/json"; axios.defaults.headers.common["Authorization"] = `Bearer${token}`;

Most decent applications will have some authorization layer, a baseUrl , and some default headers. Technically, every api client will very likely have some defaults for every request.

This is done by setting some properties on the axios object. I’m doubtless the same can be done for any client of your choosing.

6. Handle loading states

if (label) { dispatch(apiStart(label)); }

A label is just a string to identify a certain network request action. Just like an action’s type.

If a label exists, the middleware will dispatch an apiStart action creator.

Here’s what the apiStart action creator looks like:

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

The action type is API_START .

Now, within your reducers you can handle this action type, to know when a request begins. I’ll show an example shortly.

Also, upon a successful or failed network request, an API_END action will also be dispatched. This is perfect for handling loading states since you know exactly when the request begins and ends.

Again, I’ll show an example shortly.

7. Make the actual network request, handle errors, and invoke callbacks

axios .request({ url: `${BASE_URL}${url}`, method, headers, [dataOrParams]: data }) .then(({ data }) => { dispatch(onSuccess(data)); }) .catch(error => { dispatch(apiError(error)); dispatch(onFailure(error)); if (error.response && error.response.status === 403) { dispatch(accessDenied(window.location.pathname)); } }) .finally(() => { if (label) { dispatch(apiEnd(label)); } });

It isn’t as complex as it looks.

axios.request is responsible for making the network request, with an object configuration passed in. These are the variables you extracted from the action payload earlier.

Upon a successful request, as seen in the then block, dispatch an apiEnd action creator.

That looks like this:

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

Within your reducer, you can listen for this and kill off any loading states as the request has ended.

After that is done, dispatch the onSuccess callback.

The onSuccess callback returns whatever action you’d love to dispatch after the network request is successful. There’s almost always a case for dispatching an action after a successful network request, e.g., to save the fetched data to the Redux store.

If an error occurs, as denoted within the catch block, also fire off the apiEnd action creator, dispatching an apiError action creator with the failed error:

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

You may have another middleware that listens for this action type and makes sure it the error hits your external logging service.

You dispatch an onFailure callback as well. Just incase you need to show some visual feedback to the user. This also works for toast notifications.

Finally, I have shown an example of handling an authentication error:

if (error.response && error.response.status === 403) { dispatch(accessDenied(window.location.pathname)); }

In this example, I dispatch an accessDenied action creator which takes in the location the user was on.

I can then handle this accessDenied action in another middleware.

You really don’t have to handle these in another middleware. They can be done within the same code block, however, for careful abstraction, it may make more sense for your project to have these concerns separated.

And that’s it!

Implementing Redux in your app? Track Redux state and actions with LogRocket Debugging React applications can be difficult, especially when there is complex state. If you’re interested in monitoring and tracking Redux state for all of your users in production, try LogRocket. https://logrocket.com/signup/ LogRocket is like a DVR for web apps, recording literally everything that happens on your site. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores. Modernize how you debug your React apps – Start monitoring for free.

The custom middleware in action

I’ll now refactor the fake medium application to use this custom middleware. The only changes to be made is to include this middleware:

import apiMiddleware from "../middleware/api"; const store = createStore(rootReducer, applyMiddleware(apiMiddleware));

And then edit the fetchArticleDetails action to return a plain object.

export function fetchArticleDetails() { return { type: API, payload: { url: "https://api.myjson.com/bins/19dtxc", method: "GET", data: null, onSuccess: setArticleDetails, onFailure: () => { console.log("Error occured loading articles"); }, label: FETCH_ARTICLE_DETAILS } }; } function setArticleDetails(data) { return { type: SET_ARTICLE_DETAILS, payload: data }; }

Note how the payload from fetchArticleDetails contains all the needed information required by the middleware.

There’s a little problem though.

Once you go beyond one action creator, it becomes a pain to write the payload object every single time. Especially when some of the values are null or have some default values.

For ease, you may abstract the creation of the action object to a new action creator called apiAction :

function apiAction({ url = "", method = "GET", data = null, onSuccess = () => {}, onFailure = () => {}, label = "" }) { return { type: API, payload: { url, method, data, onSuccess, onFailure, label } }; }

Using ES6 default parameters, note how apiAction has some sensible defaults already set.

Now, in fetchArticleDetails you can do this:

function fetchArticleDetails() { return apiAction({ url: "https://api.myjson.com/bins/19dtxc", onSuccess: setArticleDetails, onFailure:() => {console.log("Error occured loading articles")}, label: FETCH_ARTICLE_DETAILS }); }

This could even be simpler with some ES6:

const fetchArticleDetails = () => apiAction({ url: "https://api.myjson.com/bins/19dtxc", onSuccess: setArticleDetails, onFailure: () => {console.log("Error occured loading articles")}, label: FETCH_ARTICLE_DETAILS });

A lot simpler!

And the result is the same, a working application!

To see how labels can be useful for loading states, I’ll go ahead and handle the API_START and API_END action types within the reducer.

case API_START: if (action.payload === FETCH_ARTICLE_DETAILS) { return { ...state, isLoadingData: true }; } case API_END: if (action.payload === FETCH_ARTICLE_DETAILS) { return { ...state, isLoadingData: false }; }

Now, I’m setting an isLoadingData flag in the state object based on both action types, API_START and API_END

Based on that I can set up a loading state within the App component.

Here’s the result:

That worked!

Remember, the custom middleware I’ve shared here is only to serve as a good starting point for your application. Evaluate to be sure this is right for your exact situations. You may need a few tweaks depending on your specific use case.

For what it’s worth, I have used this as a starting point on fairly large projects without regretting the decision.

Conclusion

I definitely encourage you to try out the various available options for making network requests in a redux app before committing to any.

Sadly, it becomes difficult to refactor after choosing a strategy for a grown application.

In the end, it’s your team, your application, your time, and ultimately, you alone can make the choice for yourself.

Do not forget to check out the code repository on GitHub, and thanks to https://leanpub.com/redux-book, which inspired this article.

Catch you later!