Search code examples
javascriptreactjsreduxfetchredux-thunk

Cancel previous fetch request with redux-thunk


Background of the Problem:

I am building a React/Redux app that uses redux-thunk and wretch (a fetch wrapper) to handle asynchronous requests.

I have a few search actions that can vary significantly in their load times, causing undesirable behavior.

I have looked into using AbortController(), but it's either cancelling all my requests outright, or failing to cancel the previous request.

example problem:

  • Request a search for "JOHN", then request a search for "JOHNSON".
  • Results for "JOHNSON" return first, and then results for "JOHN" return later and overwrite the "JOHNSON" results.

Goal:

Initiating a request should abort previous pending requests.

example desired behavior:

  • Request a search for "JOHN", then request a search for "JOHNSON".
  • Upon initiating the request for "JOHNSON", the pending request for "JOHN" is aborted.

Code:

actions.js

The fetchData action gets called via an onClick or by other functions.

import api from '../../utils/request';
export function fetchData(params) {
  return dispatch => {
    dispatch(requestData());
    return api
      .query(params)
      .url('api/data')
      .get()
      .fetchError(err => {
        console.log(err);
        dispatch(apiFail(err.toString()));
      })
      .json(response => dispatch(receiveData(response.items, response.totalItems)))
  }
}

export function requestData() {
  return {
    type: REQUEST_DATA,
    waiting: true,
  }
}

export function receiveData(items, totalItems) {
  return {
    type: RECEIVE_DATA,
    result: items,
    totalItems: totalItems,
    waiting: false,
  }
}

export function apiFail(err) {
  return {
    type: API_FAIL,
    error: err,
    waiting: false,
  }
}

utils/request.js

This is wretch import. Wretch is a fetch wrapper so it should function similarly to fetch.

import wretch from 'wretch';

/**
 * Handles Server Error
 * 
 * @param {object}      err HTTP Error
 * 
 * @return {undefined}  Returns undefined
 */
function handleServerError(err) {
  console.error(err);
}

const api = wretch()
  .options({ credentials: 'include', mode: 'cors' })
  .url(window.appBaseUrl || process.env.REACT_APP_API_HOST_NAME)
  .resolve(_ => _.error(handleServerError))

export default api;

Attempt:

I've tried using wretch's .signal() parameter with an AbortController(), calling .abort() after the request, but that aborts all requests, causing my app to break. Example below:

import wretch from 'wretch';

/**
 * Handles Server Error
 * 
 * @param {object}      err HTTP Error
 * 
 * @return {undefined}  Returns undefined
 */
function handleServerError(err) {
  console.error(err);
}
const controller = new AbortController();

const api = wretch()
  .signal(controller)
  .options({ credentials: 'include', mode: 'cors' })
  .url(window.appBaseUrl || process.env.REACT_APP_API_HOST_NAME)
  .resolve(_ => _.error(handleServerError))

controller.abort();
export default api;

I've tried moving the logic around to various places, but it seems abort all actions or abort none of them.

Any advice as to how to go about this would be appreciated, this is critical for my team.

Thank you


Solution

  • I feel pretty silly right now, but this is what it took to get it working.

    Solution Steps:

    • Set an AbortController to the initialState of the reducer

    reducer.js

    export default (state = {
      controller: new AbortController(),
    }, action) => {
      switch (action.type) {
        ...
    
    • Get the AbortController from the state, at the beginning of the fetch action and abort it.
    • Create a new AbortController and pass it into the requestData action.
    • Pass the new AbortController into the signal() param of the wretch call.

    actions.js

    export function fetchData(params) {
      return (dispatch, getState) => {
        const { controller } = getState().reducer;
        controller.abort();
    
        const newController = new AbortController();
        dispatch(requestData(newController));
        return api
          .signal(newController)
          .query(params)
          .url('api/data')
          .get()
          .fetchError(err => {
            console.log(err);
            dispatch(apiFail(err.toString()));
          })
          .json(response => dispatch(receiveData(response.items, response.totalItems)))
      }
    }
    
    export function requestData(controller) {
      return {
        type: REQUEST_DATA,
        waiting: true,
        controller,
      }
    }
    

    In the reducer, for the case of the requestData action, set the new AbortController to the state.

    reducer.js

    case REQUEST_DATA:
      return {
        ...state,
        waiting: action.waiting,
        controller: action.controller
      };
    

    There's some additional functionality with wretch, an .onAbort() param, that allows you to dispatch other actions when the request is aborted. I haven't coded that out yet, but I figured I'd include the info for anyone else struggling with this.