@hyper-fetch/react
Version:
React hooks and utils for the hyper-fetch
194 lines (174 loc) • 6.14 kB
text/typescript
/* eslint-disable @typescript-eslint/naming-convention */
import type {
CacheValueType,
NullableType,
RequestInstance,
ExtractResponseType,
ExtractErrorType,
ExtractAdapterResolvedType,
Dispatcher,
ExtractAdapterType,
ExtractAdapterExtraType,
ResponseDetailsType,
ExtractAdapterStatusType,
ResponseType,
} from "@hyper-fetch/core";
import { scopeKey } from "@hyper-fetch/core";
import type { UseTrackedStateType } from "helpers";
import { initialState } from "helpers";
/**
* Extracts the "identity" portion of a cache key (method + endpoint with resolved params),
* stripping the query params part. Cache keys follow the format: `method_endpoint_queryParams`.
*/
const getCacheKeyIdentity = (cacheKey: string): string => {
const lastUnderscoreIndex = cacheKey.lastIndexOf("_");
if (lastUnderscoreIndex === -1) return cacheKey;
return cacheKey.substring(0, lastUnderscoreIndex);
};
/**
* Determines whether state should be cleared when the cache key changes.
*
* - `"clean"` — always clear
* - `"preserve"` — never clear
* - `"auto"` — clear when the resource identity changed (URL params), preserve when only query params changed
*/
export const getShouldClearState = (
mode: "auto" | "preserve" | "clean",
oldCacheKey: string,
newCacheKey: string,
): boolean => {
if (oldCacheKey === newCacheKey) return false;
if (mode === "clean") return true;
if (mode === "preserve") return false;
// "auto" — compare the identity part (method + endpoint with params)
return getCacheKeyIdentity(oldCacheKey) !== getCacheKeyIdentity(newCacheKey);
};
export const getDetailsState = (
state?: UseTrackedStateType<RequestInstance>,
details?: Partial<ResponseDetailsType>,
): ResponseDetailsType => {
return {
retries: state?.retries || 0,
isCanceled: false,
isOffline: false,
willRetry: false,
addedTimestamp: +new Date(),
triggerTimestamp: +new Date(),
requestTimestamp: +new Date(),
responseTimestamp: +new Date(),
...details,
};
};
export const isStaleCacheData = (staleTime: number, staleTimestamp: NullableType<Date | number>) => {
if (!staleTimestamp) return true;
return +new Date() > +staleTimestamp + staleTime;
};
export const getValidCacheData = <T extends RequestInstance>(
request: T,
initialResponse: NullableType<Partial<ExtractAdapterResolvedType<T>>>,
cacheData: NullableType<CacheValueType<ExtractResponseType<T>, ExtractErrorType<T>, ExtractAdapterType<T>>>,
): CacheValueType<ExtractResponseType<T>, ExtractErrorType<T>, ExtractAdapterType<T>> | null => {
if (cacheData) {
return cacheData;
}
if (initialResponse) {
return {
data: null,
error: null,
status: null,
success: true,
extra: null,
cached: !!request.cache,
...(initialResponse as Partial<ExtractAdapterResolvedType<T>>),
...getDetailsState(),
staleTime: 1000,
version: request.client.cache.version,
cacheKey: request.cacheKey,
scope: request.scope,
cacheTime: request.cacheTime,
requestTimestamp: initialResponse?.requestTimestamp ?? +new Date(),
responseTimestamp: initialResponse?.responseTimestamp ?? +new Date(),
};
}
return null;
};
export const getTimestamp = (timestamp?: NullableType<number | Date>) => {
return timestamp ? new Date(timestamp) : null;
};
export const getIsInitiallyLoading = <T extends RequestInstance>({
queryKey,
dispatcher,
hasState,
revalidate,
disabled,
}: {
queryKey: string;
dispatcher: Dispatcher<ExtractAdapterType<T>>;
hasState: boolean;
revalidate?: boolean;
disabled?: boolean;
}) => {
if (!revalidate && hasState) {
return false;
}
const queue = dispatcher.getQueue(queryKey);
const isInitiallyLoading = dispatcher.hasRunningRequests(queryKey) || (!queue.stopped && disabled === false);
return isInitiallyLoading;
};
export const getInitialState = <T extends RequestInstance>({
initialResponse,
dispatcher,
request,
disabled,
revalidate,
}: {
initialResponse: NullableType<Partial<ExtractAdapterResolvedType<T>>>;
dispatcher: Dispatcher<ExtractAdapterType<T>>;
request: T;
disabled?: boolean; // useFetch only
revalidate?: boolean; // useFetch only
}): UseTrackedStateType<T> => {
const { client, cacheKey, unstable_responseMapper } = request;
const { cache } = client;
const cacheData = cache.get<ExtractResponseType<T>, ExtractErrorType<T>>(scopeKey(cacheKey, request.scope));
const cacheState = getValidCacheData<T>(request, initialResponse, cacheData);
const initialLoading = getIsInitiallyLoading({
queryKey: scopeKey(request.queryKey, request.scope),
dispatcher,
disabled,
revalidate,
hasState: !!cacheState,
});
if (cacheState) {
const mappedData = unstable_responseMapper
? unstable_responseMapper(cacheState as ResponseType<any, any, ExtractAdapterType<T>>)
: cacheState;
if (mappedData instanceof Promise) {
// For the async mapper we cannot return async values
// So we have return the initial state instead
return initialState;
}
return {
data: mappedData.data,
error: mappedData.error,
status: mappedData.status as ExtractAdapterStatusType<ExtractAdapterType<T>>,
success: mappedData.success,
extra: (mappedData.extra || client.adapter.defaultExtra) as ExtractAdapterExtraType<ExtractAdapterType<T>>,
retries: cacheState.retries,
requestTimestamp: getTimestamp(cacheState.requestTimestamp),
responseTimestamp: getTimestamp(cacheState.responseTimestamp),
loading: initialLoading,
};
}
return {
data: initialState.data,
error: initialState.error,
status: initialState.status as ExtractAdapterStatusType<ExtractAdapterType<T>>,
success: initialState.success,
extra: request.client.adapter.defaultExtra as ExtractAdapterExtraType<ExtractAdapterType<T>>,
retries: initialState.retries,
requestTimestamp: getTimestamp(initialState.requestTimestamp),
responseTimestamp: getTimestamp(initialState.responseTimestamp),
loading: initialLoading,
};
};