Source

Utilities/hooks/useFetchTotalBatched.js

import { useCallback, useState, useRef } from 'react';
import { useDeepCompareEffect } from 'use-deep-compare';
import pAll from 'p-all';

const DEFAULT_BATCH_SIZE = 50;
const CONCURRENT_REQUESTS = 2;

/**
 *  @typedef {object} useFetchTotalBatchedReturn
 *
 *  @property {boolean}  loading Wether or not data is loaded
 *  @property {object}   data    Combined result of all requests
 *  @property {Function} fetch   Function to call the batched requests on demand that additional parameters for all requests
 *
 */

// TODO This hook could be made a feature of either the useComplianceQuery or the useQuery hook directly
/**
 * This hook allows to provide a fetch function that can be called to perform "batched" requests.
 * In short it means it will make as many requests/calls with the function to fill a total.
 * This "total" is taken from the meta.total or the *first* response.
 * Responses or returns of the function are expected to contain at least a "data" and a "meta" property
 * The "data" property must be an array, to be able to merge the results.
 * The "meta" property must at least (in the first response/return) contain the *total* property.
 *
 *  @param   {Function}                   [fetchFn]           An async function that accepts an offset, a limit and a params argument
 *  @param   {object}                     [options]           Hook options
 *  @param   {number}                     [options.batchSize] The limit or items to be requested per request (*Default:* 50)
 *  @param   {boolean}                    [options.skip]      Wether or not to skip (all) requests (*Default:* false)
 *  @param   {object}                     [options.params]    Parameters passed to all calls
 *
 *  @returns {useFetchTotalBatchedReturn}                     An object to use
 *
 *  @category Hooks
 *
 */
const useFetchTotalBatched = (fetchFn, options = {}) => {
  const {
    batchSize = DEFAULT_BATCH_SIZE,
    concurrency = CONCURRENT_REQUESTS,
    skip = false,
  } = options;
  const loading = useRef(false);
  const mounted = useRef(true);
  const [totalResult, setTotalResult] = useState();

  const fetch = useCallback(
    async (...args) => {
      // TODO Make this smarter
      // When working with concurrent calls to fetchBatched Promise.all/pAll
      // `loading`s will collide and only the first call will succeed
      if (!loading.current) {
        loading.current = true;
        const firstPage = await fetchFn(0, batchSize, ...args);
        const total = firstPage?.meta?.total;
        if (total > batchSize) {
          const pages = Math.ceil(total / batchSize) || 1;
          const requests = [...new Array(pages)]
            .slice(1)
            .map((_, pageIdx) => async () => {
              const page = pageIdx + 1;
              const offset = page * batchSize;
              return await fetchFn(offset, batchSize, ...args);
            });

          const results = await pAll(requests, {
            concurrency,
          });

          const allPages = [
            ...(firstPage?.data || []),
            ...(results?.reduce((acc, { data }) => [...acc, ...data], []) ||
              []),
          ];
          const newTotalResult = {
            data: allPages,
            meta: {
              total: firstPage.meta.total,
            },
          };
          mounted.current && setTotalResult(newTotalResult);
          loading.current = false;

          return newTotalResult;
        } else {
          mounted.current && setTotalResult(firstPage);
          loading.current = false;

          return firstPage;
        }
      }
    },
    [fetchFn, batchSize, concurrency],
  );

  useDeepCompareEffect(() => {
    mounted.current = true;
    !skip && fetch();

    return () => {
      mounted.current = false;
    };
  }, [skip, fetch]);

  return {
    // TODO this might be redundant... ?
    loading: typeof totalResult === 'undefined',
    // TODO Maybe consider renaming to "result" to avoid confusion with the wrapped "data" of responses.
    data: totalResult,
    fetch,
  };
};

export default useFetchTotalBatched;