/* 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;
}