export default function useResource()

in src/hooks/useResource/index.tsx [142:340]


export default function useResource<T, U>({
  url,
  wsUrl,
  initialData = null,
  subscribeToEvents = false,
  queryParams = emptyObject,
  websocketParams,
  socketParamFilter,
  updatePredicate = defaultUpdatePredicate,
  fetchAllData = false,
  onUpdate,
  onWSUpdate,
  pause = false,
  useBatching = false,
  postRequest,
  uuid,
}: HookConfig<T, U>): Resource<T> {
  const [error, setError] = useState<APIError | null>(null);
  const initData = initialData;
  const [result, setResult] = useState<DataModel<T> | null>(null);
  const [data, setData] = useState<T | null>(initData);
  const [status, statusDispatch] = useReducer(StatusReducer, { id: 0, status: 'NotAsked' });
  const q = new URLSearchParams(queryParams).toString();
  const target = apiHttp(`${url}${q ? '?' + q : ''}`);
  const [retryCounter, setRetryCounter] = useState(0);

  // Call batch update
  useInterval(() => {
    if (useBatching && onUpdate && updateBatcher[target] && (updateBatcher[target] as Array<U>).length > 0) {
      onUpdate(updateBatcher[target] as T);
      updateBatcher[target] = [];
    }
  }, 1000);

  const websocketUrl = getWebsocketUrl(url, data, wsUrl);

  const onWebsocketUpdate = useCallback(
    (event: Event<unknown>) => {
      if (pause) return;
      // If onUpdate or onWSUpdate is given, dont update stateful data object.
      if (onUpdate || onWSUpdate) {
        // On batching add item to batch object to be sent to view later
        if (useBatching) {
          if (!updateBatcher[target]) {
            updateBatcher[target] = [];
          }
          (updateBatcher[target] as Array<U>).push(event.data as U);
        } else {
          if (onWSUpdate) {
            onWSUpdate(event.data as U, event.type);
          } else if (onUpdate) {
            onUpdate((Array.isArray(initialData) ? [event.data] : event.data) as T);
          }
        }
      } else {
        if (event.type === EventType.INSERT) {
          setData((d) => (Array.isArray(d) ? [event.data].concat(d) : d) as T);
        } else if (event.type === EventType.UPDATE) {
          setData(
            (d) =>
              (Array.isArray(d)
                ? d.map((item) => (updatePredicate(item, event.data as U) ? event.data : item))
                : event.data) as T,
          );
        }
      }
    },
    [initialData, onUpdate, onWSUpdate, pause, target, updatePredicate, useBatching],
  );

  const useWebsocketQueryParams = useMemo(
    () => (socketParamFilter ? socketParamFilter(queryParams || {}) : websocketParams || queryParams),
    [queryParams, socketParamFilter, websocketParams],
  );

  useWebsocket<U>({
    url: websocketUrl,
    queryParams: useWebsocketQueryParams,
    enabled: subscribeToEvents && !pause && !!websocketUrl,
    onUpdate: onWebsocketUpdate,
    uuid,
  });

  function newError(targetUrl: string, err: APIError, id: number) {
    setLogItem(`ERROR ${targetUrl} ${JSON.stringify(err)}`);
    statusDispatch({ type: 'setstatus', id, status: 'Error' });
    setError(err);
    logWarning(`HTTP error id: ${err.id}, url: ${targetUrl}`);
  }

  const fetchData = useCallback(
    (targetUrl: string, signal: AbortSignal, cb: (isSuccess: boolean) => void, requestid: number) => {
      setLogItem(`GET SENT ${targetUrl}`);

      fetch(targetUrl, { signal })
        .then((response) => {
          if (response.status === 200) {
            response
              .json()
              .then((result: DataModel<T>) => {
                setLogItem(`GET ${response.status} ${targetUrl} ${JSON.stringify(result)}`);
                // If onUpdate, dont store data in stateful data object
                if (onUpdate) {
                  onUpdate(result.data as T, result);
                } else {
                  setData(result.data);
                  setResult(result);
                }

                // Trigger postRequest after every request if given.
                postRequest && postRequest(true, targetUrl, result);

                // If we want all data and we are have next page available we fetch it.
                // Else this fetch is done and we call the callback
                if (fetchAllData && result.links.next !== null && result.links.next !== targetUrl) {
                  fetchData(result.links.next || targetUrl, signal, cb, requestid);
                } else {
                  cb(true);
                }
              })
              .catch(() => {
                newError(targetUrl, defaultError, requestid);
              });
          } else {
            if (response.status === 403) {
              newError(targetUrl, forbiddenError, requestid);
            } else if (response.status === 404) {
              newError(targetUrl, notFoundError, requestid);
            } else {
              response
                .json()
                .then((result) => {
                  if (typeof result === 'object' && result.id) {
                    newError(targetUrl, result, requestid);
                  } else {
                    newError(targetUrl, defaultError, requestid);
                  }
                  postRequest && postRequest(false, targetUrl);
                })
                .catch((e) => {
                  console.error('Error decoding JSON for response: ', response, ': ', e);
                  newError(targetUrl, defaultError, requestid);
                  postRequest && postRequest(false, targetUrl);
                });
            }
          }
        })
        .catch((_e) => {
          newError(targetUrl, defaultError, requestid);
          postRequest && postRequest(false, targetUrl);
        });
    },
    [fetchAllData, onUpdate, postRequest, setData, setResult],
  );

  useEffect(() => {
    const abortCtrl = new AbortController();
    const signal = abortCtrl.signal;
    let fulfilled = false;

    if (!pause) {
      const requestid = Date.now();
      statusDispatch({ type: 'set', id: requestid, status: 'Loading' });

      if (!onUpdate) {
        setData(initialData);
      }

      fetchData(
        target,
        signal,
        (isSuccess) => {
          fulfilled = true;
          if (isSuccess) {
            statusDispatch({ type: 'setstatus', id: requestid, status: 'Ok' });
            setError(null);
          } else {
            newError(target, defaultError, requestid);
          }
        },
        requestid,
      );
    }

    return () => {
      if (!fulfilled) {
        abortCtrl.abort();
      }
    };
  }, [target, pause, retryCounter, onUpdate, fetchData, initialData]);

  const retry = useCallback(() => {
    setRetryCounter((val) => val + 1);
  }, []);

  const getResult = useCallback(() => result, [result]);

  return { url, target, data, error, getResult, status: status.status, retry };
}