How we connect Redux to our Services API
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}`,
});
// see how we use `action.path`. Which in our example is `/files`
const url = `https://www.someurl.com/api${action.path}`;
return fetch(url, { headers })
.then((res) => res.json())
// `action.onSuccess` was `addFiles` which is an Action Creator
.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 realised 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():
// we have a helper that upload a file to a URL
await uploadFile(file, uploadUrl);// the polling starts
let retries = 0;
let exists = false;
while (!exists && retries <= MAX_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(appStore);
// Export `myFetch`
export default myFetch;
Finally, we inject our myFetch
to be used in our services
modules instead of the default fetch
.
// Import our depdendency
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 initialising 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.
Originally posted on Onedot Blog