import type { Denormalize, DenormalizeNullable, ErrorTypes, EndpointInterface, FetchFunction, Schema, ResolveType, } from '@data-client/core'; import { ExpiryStatus } from '@data-client/core'; import { useEffect, useMemo } from 'react'; import { InteractionManager } from 'react-native'; import useCacheState from './useCacheState.js'; import useController from './useController.js'; import useFocusEffect from './useFocusEffect.native.js'; type SchemaReturn = | { data: Denormalize; loading: false; error: undefined; } | { data: DenormalizeNullable; loading: true; error: undefined } | { data: DenormalizeNullable; loading: false; error: ErrorTypes }; type AsyncReturn = | { data: E extends (...args: any) => any ? ResolveType : any; loading: false; error: undefined; } | { data: undefined; loading: true; error: undefined } | { data: undefined; loading: false; error: ErrorTypes }; /** * Use async date with { data, loading, error } (DLE) * @see https://dataclient.io/docs/api/useDLE */ export default function useDLE< E extends EndpointInterface< FetchFunction, Schema | undefined, undefined | false >, >( endpoint: E, ...args: readonly [...Parameters] ): E['schema'] extends undefined | null ? AsyncReturn : SchemaReturn; export default function useDLE< E extends EndpointInterface< FetchFunction, Schema | undefined, undefined | false >, >( endpoint: E, ...args: readonly [...Parameters] | readonly [null] ): { data: E['schema'] extends undefined | null ? undefined : DenormalizeNullable; loading: boolean; error: ErrorTypes | undefined; }; export default function useDLE< E extends EndpointInterface< FetchFunction, Schema | undefined, undefined | false >, >( endpoint: E, ...args: readonly [...Parameters] | readonly [null] ): { data: unknown; loading: boolean; error: ErrorTypes | undefined } { 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: cachedData, 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 .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, controller, key, forceFetch, state.lastReset]); // fully "valid" data will not suspend/loading even if it is not fresh const loading = expiryStatus !== ExpiryStatus.Valid && !!maybePromise; useFocusEffect(() => { // revalidating non-suspending data is low priority, so make sure it doesn't stutter animations const task = InteractionManager.runAfterInteractions(() => { if (Date.now() > expiresAt && key) { controller.fetch(endpoint, ...(args as Parameters)); } }); return () => task.cancel(); }, []); const data = useMemo(() => { // if useSuspense() would suspend, don't include entities from cache if (loading) { if (!endpoint.schema) return undefined; // TODO: use getResponse() once it just returns data return controller.getResponseMeta(endpoint, ...args, { ...state, entities: {}, }).data; } return cachedData; // key substitutes args + endpoint // we only need cacheResults, as entities are not used in this case // eslint-disable-next-line react-hooks/exhaustive-deps }, [key, cachedData, loading, cacheResults]); const error = controller.getError(endpoint, ...args, state); // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(countRef, [data]); return { data, loading, error, }; }