@hyper-fetch/react
Version:
React hooks and utils for the hyper-fetch
297 lines (263 loc) • 8.48 kB
text/typescript
import { useDidUpdate, useDidMount, useWillUnmount } from "@better-hooks/lifecycle";
import { useDebounce, useThrottle } from "@better-hooks/performance";
import {
RequestInstance,
ExtractAdapterStatusType,
ExtractAdapterType,
ExtractAdapterExtraType,
} from "@hyper-fetch/core";
import { useRef } from "react";
import { useRequestEvents, useTrackedState } from "helpers";
import { UseFetchOptionsType, useFetchDefaultOptions, UseFetchReturnType } from "hooks/use-fetch";
import { useProvider } from "provider";
import { getBounceData } from "utils";
/**
* This hooks aims to retrieve data from server.
* @param request Request instance
* @param options Hook options
* @returns
*/
export const useFetch = <R extends RequestInstance>(
request: R,
options?: UseFetchOptionsType<R>,
): UseFetchReturnType<R> => {
// Build the configuration options
const { config: globalConfig } = useProvider();
const {
dependencies,
disabled,
dependencyTracking,
revalidate,
initialResponse,
refresh,
refreshTime,
refetchBlurred,
refetchOnBlur,
refetchOnFocus,
refetchOnReconnect,
bounce,
bounceType,
bounceTime,
bounceTimeout,
deepCompare,
} = {
...useFetchDefaultOptions,
...globalConfig.useFetchConfig,
...options,
};
const updateKey = JSON.stringify(request.toJSON());
const requestDebounce = useDebounce({ delay: bounceTime });
const requestThrottle = useThrottle({ interval: bounceTime, timeout: bounceTimeout });
const refreshDebounce = useDebounce({ delay: refreshTime });
const { cacheKey, queryKey, client } = request;
const { cache, fetchDispatcher: dispatcher, appManager, loggerManager } = client;
const ignoreReact18DoubleRender = useRef(true);
const logger = useRef(loggerManager.initialize(client, "useFetch")).current;
const bounceData = bounceType === "throttle" ? requestThrottle : requestDebounce;
const bounceFunction = bounceType === "throttle" ? requestThrottle.throttle : requestDebounce.debounce;
/**
* State handler with optimization for re-rendering, that hooks into the cache state and dispatchers queues
*/
const [state, actions, { setRenderKey, setCacheData, getStaleStatus, getIsDataProcessing }] = useTrackedState<R>({
logger,
request: request as unknown as R,
dispatcher,
initialResponse,
deepCompare,
dependencyTracking,
disabled,
revalidate,
});
/**
* Handles the data exchange with the core logic - responses, loading, downloading etc
*/
const [callbacks, listeners] = useRequestEvents<R>({
logger,
actions,
request: request as unknown as R,
dispatcher,
setCacheData,
getIsDataProcessing,
});
const { addCacheDataListener, addLifecycleListeners, clearCacheDataListener } = listeners;
// ******************
// Fetching
// ******************
const handleFetch = () => {
if (!disabled) {
logger.debug({ title: `Fetching data`, type: "system", extra: { request } });
dispatcher.add(request as unknown as R);
} else {
logger.debug({ title: `Cannot add to fetch queue`, type: "system", extra: { disabled } });
}
};
// ******************
// Refreshing
// ******************
function handleRefresh() {
if (!refresh) {
refreshDebounce.reset();
return;
}
refreshDebounce.debounce(() => {
const isBlurred = !appManager.isFocused;
// If window tab is not active should we refresh the cache
const isFetching = dispatcher.hasRunningRequests(request.queryKey);
const isQueued = dispatcher.getIsActiveQueue(request.queryKey);
const isActive = isFetching || isQueued;
const canRefreshBlurred = isBlurred && refetchBlurred && !isActive;
const canRefreshFocused = !isBlurred && !isActive;
if (canRefreshBlurred || canRefreshFocused) {
handleFetch();
logger.debug({ title: `Performing refresh request`, type: "system", extra: { request } });
}
// Start new refresh counter
handleRefresh();
});
}
const refetch = () => {
handleFetch();
handleRefresh();
};
// ******************
// Fetching lifecycle
// ******************
// This help us to check for currently running requests
const getIsFetchingIdentity = () => {
// We need to check if the queryKey have the same cacheKey elements ongoing
return dispatcher.getRunningRequests(queryKey).some((running) => running.request.cacheKey === cacheKey);
};
const initialFetchData = () => {
const hasStaleData = getStaleStatus();
const isFetching = getIsFetchingIdentity();
if ((revalidate || hasStaleData) && !isFetching) {
handleFetch();
}
};
const updateFetchData = () => {
const hasStaleData = getStaleStatus();
const shouldUpdate = !revalidate ? hasStaleData : true;
/**
* This is a hack to avoid double rendering in React 18
* It renders initial mount event and allow us to consume only hook updates
*/
if (!ignoreReact18DoubleRender.current && shouldUpdate) {
/**
* While debouncing we need to make sure that first request is not debounced when the cache is not available
* This way it will not wait for debouncing but fetch data right away
*/
if (bounce) {
logger.debug({ title: `Bounce request with ${bounceType}`, type: "system", extra: { queryKey, request } });
bounceFunction(() => handleFetch());
} else {
handleFetch();
}
} else {
ignoreReact18DoubleRender.current = false;
}
};
// ******************
// Events
// ******************
const handleMountEvents = () => {
addCacheDataListener(request as unknown as R);
addLifecycleListeners(request as unknown as R);
const focusUnmount = appManager.events.onFocus(() => {
if (refetchOnFocus) {
handleFetch();
handleRefresh();
}
});
const blurUnmount = appManager.events.onBlur(() => {
if (refetchOnBlur) {
handleFetch();
handleRefresh();
}
});
const onlineUnmount = appManager.events.onOnline(() => {
if (refetchOnReconnect) {
handleFetch();
handleRefresh();
}
});
const invalidateUnmount = cache.events.onInvalidateByKey(cacheKey, handleFetch);
const deletionUnmount = cache.events.onDeleteByKey(cacheKey, handleFetch);
const unmount = () => {
clearCacheDataListener();
focusUnmount();
blurUnmount();
onlineUnmount();
invalidateUnmount();
deletionUnmount();
};
return unmount;
};
// ******************
// Lifecycle
// ******************
/**
* Initialization of the events related to data exchange with cache and queue
* This allows to share the state with other hooks and keep it related
*/
useDidUpdate(handleMountEvents, [updateKey], true);
/**
* Initial fetch triggered once data is stale or we use the refetch strategy
*/
useDidMount(initialFetchData);
/**
* Fetching logic for updates handling
*/
useDidUpdate(updateFetchData, [updateKey, disabled, ...dependencies], true);
/**
* Refresh lifecycle handler
*/
useDidUpdate(handleRefresh, [updateKey, ...dependencies, disabled, refresh, refreshTime], true);
/**
* Reset the ignore flag for React 18 strict mode
*/
useWillUnmount(() => {
ignoreReact18DoubleRender.current = true;
});
return {
get data() {
setRenderKey("data");
return state.data;
},
get error() {
setRenderKey("error");
return state.error;
},
get loading() {
setRenderKey("loading");
return state.loading;
},
get status() {
setRenderKey("status");
return state.status as ExtractAdapterStatusType<ExtractAdapterType<R>>;
},
get success() {
setRenderKey("success");
return state.success;
},
get extra() {
setRenderKey("extra");
return state.extra as ExtractAdapterExtraType<ExtractAdapterType<R>>;
},
get retries() {
setRenderKey("retries");
return state.retries;
},
get responseTimestamp() {
setRenderKey("responseTimestamp");
return state.responseTimestamp;
},
get requestTimestamp() {
setRenderKey("requestTimestamp");
return state.requestTimestamp;
},
bounce: getBounceData(bounceData),
...actions,
...(callbacks as any),
refetch,
};
};