Dark Mode

Making API Calls using Axios and Redux-Observable

api calls using axios and redux-observable

Making API calls using Axios and Redux-Observable has proven useful for me, especially when it requires – polling an API or request cancellation or making multiple sequential requests, or making multiple parallel requests.

This article demonstrates how all of the above-mentioned things can be done with Axios and Redux-Observable.

If you are looking to setup redux-observable in your project, follow my article: Clean React Architecture with Redux, Immer, TypeScript, and Redux Observable.

1. Simple API Request

First, we will start by making simple API calls using Axios and redux-observable.

We will create an Axios instance to have support for interceptors and baseUrl. It is useful to keep the baseUrl configuration in one place and intercept the request and add an auth token.

export const api = axios.create({
  baseURL: 'http://api.yourserver.com'
});

To make an API request using redux-observable, we need four action creators.

export function loadData(requestBody) {
  return {
    type: 'api/loadData',
    payload: {
     requestBody
    }
  }
}

export function loadingData() {
  return {
    type: 'api/loadingData'
  }
}

export function loadedData(response) {
  return {
    type: 'api/loadedData',
    payload: {
      response
    }
  }
}

export function loadDataFailed(error) {
  return {
    type: 'api/loadDataFailed',
    payload: {
      error
    }
  }
}

We can now write an epic that listens for api/loadData action and send an API request using the Axios instance that we created earlier.

import { api } from './axios';

const loadDataEpic = (action$, state$) => action$.pipe(
  ofType('api/loadData'),
  mergeMap(action => from(api.post('/login', action.payload.requestBody)).pipe(
    map(response => loadedData(response.data),
    catchError(err => of(loadDataFailed(err)),
    startWith(loadingData())
  )
);

2. Polling an API with Axios

To poll an API, we might need two more actions, one to start the polling and another to stop the polling.

export function startPollingData() {
  return {
    type: 'api/startPollingData'
  }
}

export function stopPollingData() {
  return {
    type: 'api/stopPollingData'
  }
}

When we receive the api/startPollingData action, we can start a timer using the timer operator in RxJS.

Every time, the timer emits we can dispatch the loadData action which will make the API call.

Finally, to stop the polling when 'api/stopPollingData action is dispatched we can use the takeUntil operator.

import { api } from './axios';

const pollingDataEpic = (action$, state$) =>
  action$.pipe(
    ofType('api/startPollingData'),
    switchMap(action =>
      timer(0, 5000).pipe(
        takeUntil(action$.ofType('api/stopPollingData')),
        map(() => loadData({})))
      )
    )
  );

Once the polling is stopped, it can be restarted by simply dispatching api/startPollingData action.

3. Axios Request Cancellation

If we replace the mergeMap with a switchMap, RxJS will unsubscribe from the previous Observable.

Ideally, the following code should cancel any pending Axios requests when api/loadData action is dispatched. But that is not the case when using Axios.

import { api } from './axios';

const loadDataEpic = (action$, state$) => action$.pipe(
  ofType('api/loadData'),
  switchMap(action => from(api.post('/login', action.payload.requestBody)).pipe(
    map(response => loadedData(response.data),
    catchError(err => of(loadDataFailed(err)),
    startWith(loadingData())
  )
);

Canceling Axios API requests with RxJS is a little tricky. Let us take a step backward and understand the working of Observable.

An Observable has a subscribe function, and it is what gets executed when the Observable is subscribed. It also returns a function that is executed when it is unsubscribed.

new Observable(subscriber => {
  // Code that gets executed when observable is subscribed.
  return () => {
    // Code that gets executed when observable is unsubscribed.
  }
});

When we convert an Axios promise into an Observable using the from operator, the resulting Observable’s unsubscribe function won’t have any code that can cancel the Axios request.

We can solve this problem by writing a custom function to convert the Axios promise into an Observable. Like so,

import { api } from './axios';
import axios from 'axios';

export function customFrom(requestConfig) {
   const obs = new Observable(subscriber => {
     const source = axios.CancelToken.source();
     api({ …requestConfig, cancelToken: source.token })
       .then(response => {
         subscriber.next(response);
         subscriber.complete();
       })
       .catch(error => {
         subscriber.error(error);
       });
     return () => {
       source.cancel();
     };
   });
   return obs;
}

Finally, we can replace the from with customFrom to properly cancel the pending requests.

const loadDataEpic = (action$, state$) => action$.pipe(
  ofType('api/loadData'),
  switchMap(action => customFrom(api.post('/login', action.payload.requestBody)).pipe(
    map(response => loadedData(response.data),
    catchError(err => of(loadDataFailed(err)),
    startWith(loadingData())
  )
);

4. Axios Parallel Requests

To make multiple API requests parallelly or simultaneously using Axios and wait for them to complete, we can use the forkJoin operator. It is very similar to Promise.all.

export const loadDataEpic = (action$, state$) =>
   action$.pipe(
     ofType('api/loadData'),
     mergeMap(action => {
       return forkJoin([axios.post('/api'), axios.post('/login')]).pipe(
         map(([apiData, loginData]) => loadedData({ apiData, loginData })),
         catchError(() => of(loadDataFailed())),
         startWith(loadingData())
       );
     })
   );

We can pass multiple promises to forkJoin, and it will wait for all the promises to be resolved and then emit an array of data.

That should cover most of the common use cases for making API requests. If not, please keep in mind you can always mix and match the operators to get the desired behavior. Just take little help from our friend – documentation.

See responses (1)