import { ExpiryStatus } from '@data-client/core'; import type { EndpointInterface, Denormalize, Schema, FetchFunction, DenormalizeNullable, ResolveType, } from '@data-client/core'; import { useEffect, useMemo } from 'react'; import useCacheState from './useCacheState.js'; import useController from './useController.js'; /** * Ensure an endpoint is available. * Suspends until it is. * * `useSuspense` guarantees referential equality globally. * @see https://dataclient.io/docs/api/useSuspense * @throws {Promise} If data is not yet available. * @throws {NetworkError} If fetch fails. */ export default function useSuspense< E extends EndpointInterface< FetchFunction, Schema | undefined, undefined | false >, >( endpoint: E, ...args: readonly [...Parameters] ): E['schema'] extends undefined | null ? ResolveType : Denormalize; export default function useSuspense< E extends EndpointInterface< FetchFunction, Schema | undefined, undefined | false >, >( endpoint: E, ...args: readonly [...Parameters] | readonly [null] ): E['schema'] extends undefined | null ? ResolveType | undefined : DenormalizeNullable; export default function useSuspense< E extends EndpointInterface< FetchFunction, Schema | undefined, undefined | false >, >( endpoint: E, ...args: readonly [...Parameters] | readonly [null] ): unknown { const state = useCacheState(); const controller = useController(); const key = args[0] !== null ? endpoint.key(...args) : ''; const cacheResults = key && state.endpoints[key]; const meta = state.meta[key]; // Compute denormalized value const { data, expiryStatus, expiresAt, countRef } = useMemo(() => { return controller.getResponseMeta(endpoint, ...args, state); // eslint-disable-next-line react-hooks/exhaustive-deps }, [ cacheResults, state.indexes, state.entities, state.entitiesMeta, meta, key, ]); // If we are hard invalid we must fetch regardless of triggering or staleness const forceFetch = expiryStatus === ExpiryStatus.Invalid; const maybePromise = useMemo(() => { // null params mean don't do anything if ((Date.now() <= expiresAt && !forceFetch) || !key) return; return ( controller // if args is [null], we won't get to this line .fetch(endpoint, ...(args as Parameters)) .catch(() => {}) ); // we need to check against serialized params, since params can change frequently // eslint-disable-next-line react-hooks/exhaustive-deps }, [expiresAt, key, forceFetch, state.lastReset]); // fully "valid" data will not suspend even if it is not fresh if (expiryStatus !== ExpiryStatus.Valid && maybePromise) { throw maybePromise; } const error = controller.getError(endpoint, ...args, state); if (error) throw error; // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(countRef, [data]); return data; }