/* eslint-disable @typescript-eslint/ban-ts-comment */ import type { ErrorTypes } from '@rest-hooks/core'; import { denormalizeCached } from '@rest-hooks/normalizr'; import type { Schema, Denormalize, DenormalizeNullable, EndpointInterface, FetchFunction, } from '@rest-hooks/react'; import { ExpiryStatus } from '@rest-hooks/react'; import { StateContext, __INTERNAL__, useController } from '@rest-hooks/react'; import { useContext, useMemo } from 'react'; import type { ParamsFromShape, ReadShape } from './endpoint/index.js'; import shapeToEndpoint from './endpoint/shapeToEndpoint.js'; const { inferResults } = __INTERNAL__; type CondNull = P extends null ? A : B; type StatefulReturn = CondNull< P, { data: DenormalizeNullable; loading: false; error: undefined; }, | { data: Denormalize; loading: false; error: undefined; } | { data: DenormalizeNullable; loading: true; error: undefined } | { data: DenormalizeNullable; loading: false; error: ErrorTypes } >; /** * Ensure a resource is available; loading and error returned explicitly. * @see https://resthooks.io/docs/guides/no-suspense */ export default function useStatefulResource< E extends | EndpointInterface | ReadShape, Args extends | (E extends (...args: any) => any ? readonly [...Parameters] : readonly [ParamsFromShape]) | readonly [null], >(endpoint: E, ...args: Args): StatefulReturn { const state = useContext(StateContext); const controller = useController(); const adaptedEndpoint: EndpointInterface< FetchFunction, Schema | undefined, undefined > = useMemo(() => { return shapeToEndpoint(endpoint) as any; // we currently don't support shape changes // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const key = args[0] !== null ? adaptedEndpoint.key(...args) : ''; const cacheResults = args[0] !== null && state.results[key]; // Compute denormalized value // eslint-disable-next-line prefer-const let { data, expiryStatus, expiresAt } = useMemo(() => { return controller.getResponse(adaptedEndpoint, ...args, state) as { data: DenormalizeNullable | undefined; expiryStatus: ExpiryStatus; expiresAt: number; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [ cacheResults, state.indexes, state.entities, state.entityMeta, state.meta, key, ]); const error = controller.getError(adaptedEndpoint, ...args, state); // 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.fetch(adaptedEndpoint, ...(args as any)).catch(() => {}); // we need to check against serialized params, since params can change frequently // eslint-disable-next-line react-hooks/exhaustive-deps }, [expiresAt, controller, key, forceFetch, state.lastReset]); // fully "valid" data will not suspend/loading even if it is not fresh const loading = expiryStatus !== ExpiryStatus.Valid && !!maybePromise; if (loading && adaptedEndpoint.schema) data = denormalizeCached( inferResults( adaptedEndpoint.schema, args as any, state.indexes, state.entities, ), adaptedEndpoint.schema, {}, ).data as any; return { data, loading, error, } as any; }