Using drivers

What is driver?

You must have noticed by now that this library uses a concept called drivers. So what is driver exactly? Technically, driver is just a function, which receives a request config, sends an AJAX request and returns a promise, which will be resolved for success case and rejected for error or abort cases. Practically, driver is an abstraction, a way to use this library with any way to communicate with servers. Some people like Axios, other Fetch API. Some people like REST, other prefer GraphQL. Thanks to drivers concept you can use anything, even in combination in one app - that's it, it is possible to use multiple drivers at the same time! Anyway, this library provides many built-in drivers, but it is also possible to write your own, which will be covered later in this chapter.

How to use drivers

You must use at least one driver. You can choose one of provided drivers by this library or write your own. Let's assume we pick fetch driver. Install it:

$ npm install @redux-requests/fetch

and pass it to handleRequests:

import 'isomorphic-fetch';
import { handleRequests } from '@redux-requests/core';
import { createDriver } from '@redux-requests/fetch';
handleRequests({
driver: createDriver(window.fetch, {
baseURL: 'https://my-domain.com',
AbortController: window.AbortController,
}),
});

And that's it, fetch driver is ready to use and the library will understand Fetch API config in request actions.

Multiple drivers

You can use multiple drivers at the same time if you need it. For example, if you want to use Axios by default, but also Fetch API sometimes, you can do it like this:

import axios from 'axios';
import 'isomorphic-fetch';
import { handleRequests } from '@redux-requests/core';
import { createDriver as createAxiosDriver } from '@redux-requests/axios';
import { createDriver as createFetchDriver } from '@redux-requests/fetch';
handleRequests({
driver: {
default: createAxiosDriver(axios),
fetch: createFetchDriver(window.fetch, {
baseURL: 'https://my-domain.com',
AbortController: window.AbortController,
}),
},
});

As you can see, the default driver is Axios, so how to mark a request to be run by Fetch driver? Just pass the key you assigned Fetch driver to (fetch in our case) in action.meta.driver, for instance:

const fetchUsers = () => ({
type: 'FETCH_USERS',
request: {
url: '/users/',
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json',
},
},
meta: {
driver: 'fetch',
},
});

Writing your own driver

As mentioned earlier, driver is just a function, which receives a request config, sends an AJAX request and returns a promise, which will be resolved for success case and rejected for error or abort cases.

So, let's write axios driver. In order to understand what will happen next, it is recommended to get familiar with axios library, especially how to abort requests. Anyway, let's start some coding:

import axios from 'axios';
const axiosDriver = requestConfig => {
return axios(requestConfig).then(response => ({ data: response.data }));
};

Well, it wasn't so difficult, was it? As we can see, we just wrote a function which get's requestConfig (it will be passed from action.request) and returns called axios with it. axios(requestConfig) already returns a promise which will be rejected for error, so we are good here. The only thing we did is adding .then(response => ({ data: response.data })) to resolve promises only with data - the library expects promises to be resolved with object with at least data.

Supporting more than just data

As written above, for success response promise has to be resolved with an object with at least data key, but you can add anything else:

import axios from 'axios';
const axiosDriver = requestConfig => {
return axios(requestConfig).then(response => ({
data: response.data,
headers: response.headers,
status: response.status,
}));
};

Now headers and status will be available in onSuccess interceptor and in promise which is returned by request action dispatch (next to data). However, note that still only data will be stored in reducer, so if you need to access a header for instance from Redux state, you can store it in your own reducer or you could merge a header with data inside onSuccess interceptor.

Supporting aborts in custom drivers

We are not done yet though, our driver does not support aborts yet, let's fix that:

import axios from 'axios';
const axiosDriver = requestConfig => {
const abortSource = axios.CancelToken.source();
const responsePromise = axios({
cancelToken: abortSource.token,
...requestConfig,
})
.then(response => ({
data: response.data,
headers: response.headers,
status: response.status,
}))
.catch(error => {
if (axios.isCancel(error)) {
throw 'REQUEST_ABORTED';
}
throw error;
});
responsePromise.cancel = () => abortSource.cancel();
return responsePromise;
};

This looks a little more complicated, but it is not, let's analyze above code steps by steps. First of all, to support abort we decorated returned promise with cancel method, which will be called by the library when a request should be aborted. Native promises cannot be aborted/cancelled, but by adding this we make it possible. This technique is quite similar for example to Bluebird promises. Moreover, notice catch logic. This uses axios helper to check whether request promise was rejected due to abort or another error, and in case of abort, we throw special error REQUEST_ABORTED, so that the library can know that promise was rejected due to cancellation. This is needed because we need to handle 3 response types - success, error or abort, while promise can be just resolved or rejected. We could also use observables instead of promises as building blocks for drivers, but they are less popular than promises and they require libraries/polyfills installed. Hence the decision for such API. Also, you need to remember not to use async function! If you do, javascript engine would wrap your returned promise and .cancel method would be gone! So resist the temptation and stick just to promises when writing drivers!

Making your driver configurable

Most of the time you would probably want your driver to be configurable. For instance, we might want to allow to pass a custom axios instance to axios driver. So let's refactor what we have:

import axios from 'axios';
const createAxiosDriver = axiosInstance => requestConfig => {
const abortSource = axios.CancelToken.source();
const responsePromise = axiosInstance({
cancelToken: abortSource.token,
...requestConfig,
})
.then(response => ({
data: response.data,
headers: response.headers,
status: response.status,
}))
.catch(error => {
if (axios.isCancel(error)) {
throw 'REQUEST_ABORTED';
}
throw error;
});
responsePromise.cancel = () => abortSource.cancel();
return responsePromise;
};

Now we could create driver with a configured axios, like:

import axios from 'axios';
const axiosDriver = createAxiosDriver(
axios.create({
baseURL: 'https://some-domain.com/api/',
}),
);

So basically we refactored axiosDriver into createAxiosDriver - function which returns axiosDriver. This technique is of course not mandatory but it might be handy to make your drivers more flexible.

Supporting download and upload progress

Optionally drivers could support download and upload progress. Because axios makes it easy with the help of ProgressEvent, let's see how we could implement it:

import axios from 'axios';
const calculateProgress = progressEvent =>
parseInt((progressEvent.loaded / progressEvent.total) * 100);
const createAxiosDriver = axiosInstance => (
requestConfig,
requestAction,
driverActions,
) => {
const abortSource = axios.CancelToken.source();
const responsePromise = axiosInstance({
cancelToken: abortSource.token,
onDownloadProgress:
driverActions.setDownloadProgress &&
(progressEvent => {
if (progressEvent.lengthComputable) {
driverActions.setDownloadProgress(calculateProgress(progressEvent));
}
}),
onUploadProgress:
driverActions.setUploadProgress &&
(progressEvent => {
if (progressEvent.lengthComputable) {
driverActions.setUploadProgress(calculateProgress(progressEvent));
}
}),
...requestConfig,
})
.then(response => ({
data: response.data,
headers: response.headers,
status: response.status,
}))
.catch(error => {
if (axios.isCancel(error)) {
throw 'REQUEST_ABORTED';
}
throw error;
});
responsePromise.cancel = () => abortSource.cancel();
return responsePromise;
};

As you can see, you could utilise driverActions helpers which are passed to any driver. We use them in axios onDownloadProgress and onUploadProgress callbacks. After adding this, you could add meta.measureDownloadProgress or meta.measureUploadProgress to a request action and you could access downloadProgress or uploadProgress values from selectors like getQuery or getMutation.

Last updated on by Konrad