How we connect Redux to our Services API

Disclaimer: This article assumes a working understanding of Javascript and Redux.

We would like to share how we connect our React and Redux Application with our Services API. It's been an interesting journey and we are happy with what we have so far.

First of all, you need to understand that we use authentication with JWT. We send the token in the Authorization Header in every request.After a first request where we get the token, we store it in the Redux State. In all the subsequent requests, we need to retrieve the token and add it to the headers of the request.

First approach

Our first approach to solve the problem was to use a custom Redux Middleware.

The idea is to dispatch an action with an object similar to the configuration object used in $.ajax. Then this object gets intercepted by the Middleware and it makes the request.

This is how an Action Creator could look like:

const getFiles = () => ({
 type: API,
 path: '/files',
 onSuccess: addFiles,
});

Where addFiles is another Action Creator that will be dispatched with the response of the call to /files.

const addFiles = (files) => ({
 type: ADD_FILES,
 payload: files,
});

The Middleware is the responsible for reading the user token from Redux State, create the Headers with it and make a request using the path property in the action.

This is a simple example of how the Middleware could look like:

const middleware = ({ dispatch, getState }) => (next) => (action) => {
 if (action.type !== API) {
   return next(action);
 }
 const token = getState().user.token;
 const headers = new Headers({
   Authorization: `Bearer ${token}`,
 });
 const url = `https://www.someurl.com/api${action.path}`;
 return fetch(url, { headers })
   .then((res) => res.json())
   .then((data) => dispatch(action.onSuccess(data)));
}

Problems with the first approach

The custom middleware was working just fine for simple interactions with the API. However, it was getting tricky to use with more complex flows. In one specific flow, after a file is uploaded, we keep polling our Services API to make sure that the upload to S3 was satisfactory.

This means that after the initial request, we want to keep making requests one after another until one response is true or we get to a maximum number of retries. This flow is not very complex. However, we realized it was already hard to read with our custom middleware.

There is no need to go into the details of how we solved this with the custom middleware. But it was not an elegant solution.

Back to basics

We realized we have had the answer in front of our noses the whole time. We were using redux-thunk. This was our answer. We could use an async function to convert the polling into a synchronous code using a while loop and await.

const getFiles = () => async ({ dispatch, getState }) => {
 const token = getState().user.token;
 const headers = new Headers({
   Authorization: `Bearer ${token}`,
 });
 const url = 'https://www.someurl.com/api/files';
 const response = await fetch(url, { headers });
 const files = await res.json();
 const finalAction = addFiles(files);
 return dispatch(finalAction);
}

Remember that in each request we also need to add the token to the Header. Doing that in every Thunk Action is not a very elegant solution. Neither to have the whole url.

Solution: Services Module

We realized that we needed a module to talk to the Services API.

We wanted a module that could be used in the following way:

const getFiles = () => async ({ dispatch }) => {
 const response = await services.getFiles();
 const files = await res.json();
 const finalAction = addFiles(files);
 return dispatch(finalAction);
};

This action is simple and easy to read. But more important, these Thunk Actions allow us to create complex flows easily. For example, the polling could be something like this:

const uploadFile = (file) => async ({ dispatch }) => {
 const response = await services.getUploadUrl(file);
 const { newFile, uploadUrl } = await response.json():
 await uploadFile(file, uploadUrl);
 let retries = 0;
 let exists = false;
 while (!exists && retries
   exists = await services.fileExists(newFile.id);
   retries += 1;
 }
 if (!exists) {
   return dispatch(errorNotification('File did not upload successfully, please try again'))
 }
 return dispatch(addFile(newFile));
};

The uploadFile action is as easy to read as the previous getFiles action, even though the flow is more complex.However, we still need to figure out where to read the token from Redux State.

Dependency Injection

We are in the browser. Which means that our services module is going to use fetch to make the requests.

const services = {
 getFiles: () => {
   const url = 'https://www.someurl.com/api/files';
   return fetch(url);
 },
};

Every time we use fetch, we want to access the state, retrieve the token and create the header.

Let's create our own version of fetch.

const myFetch = (url, configuration) => {
 return fetch(url, configuration)
};

Let's use a factory function to create our myFetch.

const myFetchFactory = () => (url, configuration) => {
 return fetch(url, configuration)
};
const myFetch = myFetchFactory();

Why do we do this? Good question. Keep reading.

We can pass the Redux Store to the Factory.

import appStore from 'store';

const myFetchFactory = (store) => (url, configuration) => {
 return fetch(url, configuration)
};
const myFetch = myFetchFactory(appStore);

Now we have everything we need. In our myFetchFactory we can retrieve the token from Redux State and create the Header.

const myFetchFactory = (store) => (url, configuration) => {
 const token = store.getState().user.token;
 const headers = new Headers({
   Authorization: `Bearer ${token}`,
 });
 const newConfiguration = {
   ...configuration,
   headers,
 };
 return fetch(url, newConfiguration);
};

const myFetch = myFetchFactory(store);
export default myFetch;

Finally, we inject our myFetch to be used in our services modules instead of the default fetch.

// Import our dependency
import myFetch from './myFetch';

const servicesFactory = (myFetch) => ({
 getFiles: () => {
   const url = 'https://www.someurl.com/api/files';
   return myFetch(url);
 },
});

const services = servicesFactory(myFetch);

Conclusion

When having complex asynchronous flows, make use of async and await for better readability. To access the store in modules that are not Thunk Actions nor Redux Middleware. Just pass the store when initializing the module.Do not hesitate to leave your comments below. We would love to hear how people solved this issue and share their best practices. Check also out open positions if this blog post resonates with you.