Search code examples
reduxredux-toolkitrtk-query

RTK Query: maxRetries && backoff


I am using RTK Query to perform my API calls. I have a mutation which I want to retry fetching if got the an error response. I also want to control the delay between the retries.

** Note: I tried maxRetries, but I have no control over the delay between retries

** Note 2: after digging deep, I found the "retryCondition" fn under "extraOptions". Also, I checked the "backoff". but I am not sure how to use these together to control the delay. I searched over the internet && found nothing about backoff Also the documentation mentions the "retry" briefly.


Solution

  • You're right, the docs don't surface this feature well enough and I had to go spelunking in the repo to find the information. The retry function takes 2 arguments, first is the base query function and second is a default options (RetryOptions) configuration object.

    See RetryOptions type declaration:

    export type RetryOptions = {
      /**
       * Function used to determine delay between retries
       */
      backoff?: (attempt: number, maxRetries: number) => Promise<void>
    } & (
      | {
          /**
           * How many times the query will be retried (default: 5)
           */
          maxRetries?: number
          retryCondition?: undefined
        }
      | {
          /**
           * Callback to determine if a retry should be attempted.
           * Return `true` for another retry and `false` to quit trying prematurely.
           */
          retryCondition?: RetryConditionFunction
          maxRetries?: undefined
        }
    )
    

    The backoff property is what you're interested in

    (attempt: number, maxRetries: number) => Promise<void>
    

    It's a function that takes attempt and max retries arguments and returns a Promise. Fortunately there's already a default backoff function you can reference:

    /**
     * Exponential backoff based on the attempt number.
     *
     * @remarks
     * 1. 600ms * random(0.4, 1.4)
     * 2. 1200ms * random(0.4, 1.4)
     * 3. 2400ms * random(0.4, 1.4)
     * 4. 4800ms * random(0.4, 1.4)
     * 5. 9600ms * random(0.4, 1.4)
     *
     * @param attempt - Current attempt
     * @param maxRetries - Maximum number of retries
     */
    async function defaultBackoff(attempt: number = 0, maxRetries: number = 5) {
      const attempts = Math.min(attempt, maxRetries)
    
      const timeout = ~~((Math.random() + 0.4) * (300 << attempts)) // Force a positive int in the case we make this an option
      await new Promise((resolve) =>
        setTimeout((res: any) => resolve(res), timeout)
      )
    }
    

    IMO this is a pretty good default function, but you could construct your own if necessary.

    Examples:

    1 second, 2 seconds, 3 seconds, ...maxRetries seconds

    const customBackOff = async (attempt = 0, maxRetries = 5) => {
      const attempts = Math.min(attempt, maxRetries);
    
      await new Promise(resolve => {
        setTimeout(resolve, attempts * 1000);
      });
    };
    

    1 second each retry

    const customBackOff = async (attempt = 0, maxRetries = 5) => {
      await new Promise(resolve => {
        setTimeout(resolve, 1000);
      });
    };
    

    Lookup in array.

    const backoffDelays = [500, 2000, 5000];
    
    const customBackOff = async (attempt = 0, maxRetries = 5) => {
      const attempts = Math.min(backoffDelays.length - 1, maxRetries);
    
      await new Promise(resolve => {
        setTimeout(resolve, backoffDelays[attempts]);
      });
    };
    

    Follow the example from the documentation to decorate your base query function.

    const customBaseQuery = retry(
      fetchBaseQuery({ baseUrl: '/' }),
      { backoff: customBackOff, maxRetries: 5 }
    );
    
    export const api = createApi({
      baseQuery: customBaseQuery,
      endpoints: (build) => ({
        ....
      }),
    });