Data fetching in Redux apps — a 100% correct approach

August 08, 2018 0 Comments

Data fetching in Redux apps — a 100% correct approach

 

 

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.

redux-thunk? redux-saga? redux-observable? redux-promise?

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.

Here’s the small application I’ll be walking you through. Github Repo

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 may 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 fascinates you.

The app with a medium clap clone

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 2 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>
);
};

Where 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 😂

In components/Clap.js :

The medium clap component is exported within this directory. 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 5 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.

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?

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 utilise 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:

The article details have been successfully fetched from the server.

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 recommedArticle. 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?

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?”

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 ?

  1. A centralised solution i.e in one module.
  2. Can handle various http methods, GET, POST, DELETE and PUT
  3. Can handle setting custom headers
  4. Supports custom error handling e.g. to be sent to some external logging service, or for handling authorisation errors.
  5. Allows for onSuccess and onFailure callbacks
  6. 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 BASEURL = process.env.REACTAPPBASEURL || "";

const defaultHeaders = {};
let headers = { ...defaultHeaders };

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

if (action.type ! API) return;

const {
url,
method,
data,
accessToken,
onSuccess,
onFailure,
label,
headersOverride
} = action.payload;
  const dataOrParams = ["GET", "DELETE"].includes(method) ? 
"params" : "data";

if (accessToken) {
headers = {
...headers,
Authorization: Bearer ${accessToken}
};
}

if (headersOverride) {
headers = {
...headers,
...headersOverride
};
}

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

axios
.request({
url: ${BASE_URL}${url},
method,
headers,
[dataOrParams]: data
})
.then(({ data }) => {
if (label) {
dispatch(apiEnd(label));
}
dispatch(onSuccess(data));
})
.catch(error => {
if (label) {
dispatch(apiEnd(label));
}
dispatch(apiError(error));
dispatch(onFailure(error));

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

export default apiMiddleware;

With barely 100 lines of code, which you can grab from GitHub, you have a customised 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:

The general flow of the custom middleware

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

The next line after the imports fetches the BASEURL from some environment variable. Depending on your set up, the way you fetch environment variables might slightly differ. Here I’m using the default way to do so in a react app.

Why extract the base url anyway?

It makes a lot of sense to ONLY have specific endpoints in your action creators as opposed to full URLS.

e.g. /animals instead of https://facebook-api.com/animals

Not hardcoding this value makes for a lot more flexibility and ease of change in the future.

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,
accessToken,
onSuccess,
onFailure,
label,
headersOverride
} = 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, accessToken refers to an authorisation token, incase your app includes an authorisation layer, onSuccess and onFailure represent any action creators you’ll like to dispatch upon successful or failed request, label refers to a string representation of the request and lastly, headersOverride represents any custom headers to overrde any defaults.

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. Populate the headers object

if (accessToken) {
headers = {
...headers,
Authorization: Bearer ${accessToken}
};
}

if (headersOverride) {
headers = {
...headers,
...headersOverride
};
}

Most decent applications will have some authorisation layer. This is a good place to populate an headers object with any special headers in the action payload, and also set defaults e.g the Authorization header as seen above.

The second conditional represents a bit of an edge case i.e a situation where you need to override some headers in a certain request. Just include a headersOverride field to the action payload and that will be taken care of as well.

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 APISTART.

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 APIEND 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 }) => {
if (label) {
dispatch(apiEnd(label));
}
dispatch(onSuccess(data));
})
.catch(error => {
if (label) {
dispatch(apiEnd(label));
}
dispatch(apiError(error));
dispatch(onFailure(error));

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

It isn’t as complex as it looks.

axios.request is responsible for making the network request, with an object configuration passed in. This includes the url, method, headers , and a data or params field depending on whether the request is a GET, DELETE request or not. 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: APIEND,
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!

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,
accessToken: null,
onSuccess: setArticleDetails,
onFailure: () => {
console.log("Error occured loading articles");
},
label: FETCHARTICLEDETAILS,
headersOverride: null
}
};
}

function setArticleDetails(data) {
return {
type: SETARTICLEDETAILS,
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,
accessToken = null,
onSuccess = () => {},
onFailure = () => {},
label = "",
headersOverride = null
}) {
return {
type: API,
payload: {
url,
method,
data,
accessToken,
onSuccess,
onFailure,
label,
headersOverride
}
};
}

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: FETCHARTICLEDETAILS
});
}

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: FETCHARTICLEDETAILS
});

A lot simpler!

And the result is the same, a working application!

a working application :)

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

case APISTART:
if (action.payload = FETCH
ARTICLEDETAILS) {
return {
...state,
isLoadingData: true
};
}
case APIEND:
if (action.payload === FETCHARTICLEDETAILS) {
return {
...state,
isLoadingData: false
};
}

Now, I’m setting a isLoadingData flag in the state object based on both action types, APISTART and APIEND

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

Here’s the result:

See the “loading ….” text ?

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.

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.

Catch you later!

LogRocket is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single page apps.

Try it for free.


Tag cloud