@hyper-fetch/react
Version:
React hooks and utils for the hyper-fetch
305 lines (270 loc) • 9.25 kB
text/typescript
import { useRef } from "react";
import { useDidUpdate, useDidMount, useWillUnmount } from "@better-hooks/lifecycle";
import { useDebounce, useThrottle } from "@better-hooks/performance";
import { RequestInstance, Request, getRequestKey } from "@hyper-fetch/core";
import { useRequestEvents, useTrackedState } from "helpers";
import { UseFetchOptionsType, useFetchDefaultOptions, UseFetchReturnType } from "hooks/use-fetch";
import { useConfigProvider } from "config-provider";
import { getBounceData } from "utils";
import { InvalidationKeyType } from "types";
/**
* This hooks aims to retrieve data from server.
* @param request Request instance
* @param options Hook options
* @returns
*/
export const useFetch = <RequestType extends RequestInstance>(
request: RequestType,
options: UseFetchOptionsType<RequestType> = useFetchDefaultOptions,
): UseFetchReturnType<RequestType> => {
// Build the configuration options
const [globalConfig] = useConfigProvider();
const {
dependencies = useFetchDefaultOptions.dependencies,
disabled = useFetchDefaultOptions.disabled,
dependencyTracking = useFetchDefaultOptions.dependencyTracking,
revalidate = useFetchDefaultOptions.revalidate,
initialData = useFetchDefaultOptions.initialData,
refresh = useFetchDefaultOptions.refresh,
refreshTime = useFetchDefaultOptions.refreshTime,
refetchBlurred = useFetchDefaultOptions.refetchBlurred,
refetchOnBlur = useFetchDefaultOptions.refetchOnBlur,
refetchOnFocus = useFetchDefaultOptions.refetchOnFocus,
refetchOnReconnect = useFetchDefaultOptions.refetchOnReconnect,
bounce = useFetchDefaultOptions.bounce,
bounceType = useFetchDefaultOptions.bounceType,
bounceTime = useFetchDefaultOptions.bounceTime,
// TODO: handle type error
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
bounceTimeout = useFetchDefaultOptions.bounceTime,
deepCompare = useFetchDefaultOptions.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, queueKey, client } = request;
const { cache, fetchDispatcher: dispatcher, appManager, loggerManager } = client;
const ignoreReact18DoubleRender = useRef(true);
const logger = useRef(loggerManager.init("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<RequestType>({
logger,
request,
dispatcher,
initialData,
deepCompare,
dependencyTracking,
disabled,
revalidate,
});
/**
* Handles the data exchange with the core logic - responses, loading, downloading etc
*/
const [callbacks, listeners] = useRequestEvents({
logger,
actions,
request,
dispatcher,
setCacheData,
getIsDataProcessing,
});
const { addCacheDataListener, addLifecycleListeners, clearCacheDataListener } = listeners;
// ******************
// Fetching
// ******************
const handleFetch = () => {
if (!disabled) {
logger.debug(`Fetching data`);
dispatcher.add(request);
} else {
logger.debug(`Cannot add to fetch queue`, { 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.queueKey);
const isQueued = dispatcher.getIsActiveQueue(request.queueKey);
const isActive = isFetching || isQueued;
const canRefreshBlurred = isBlurred && refetchBlurred && !isActive;
const canRefreshFocused = !isBlurred && !isActive;
if (canRefreshBlurred || canRefreshFocused) {
handleFetch();
logger.debug(`Performing refresh request`);
}
// Start new refresh counter
handleRefresh();
});
}
const handleInvalidation = (invalidateKey: InvalidationKeyType) => {
if (invalidateKey && invalidateKey instanceof Request) {
cache.invalidate(getRequestKey(invalidateKey));
} else if (invalidateKey && !(invalidateKey instanceof Request)) {
cache.invalidate(invalidateKey);
}
};
const refetch = (invalidateKey?: InvalidationKeyType | InvalidationKeyType[]) => {
if (invalidateKey && Array.isArray(invalidateKey)) {
invalidateKey.forEach(handleInvalidation);
} else if (invalidateKey && !Array.isArray(invalidateKey)) {
handleInvalidation(invalidateKey);
} else {
handleFetch();
handleRefresh();
}
};
// ******************
// Fetching lifecycle
// ******************
// This help us to check for currently running requests
const getIsFetchingIdentity = () => {
// We need to check if the queueKey have the same cacheKey elements ongoing
return dispatcher.getRunningRequests(queueKey).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(`Bounce request with ${bounceType}`, { queueKey, request });
bounceFunction(() => handleFetch());
} else {
handleFetch();
}
} else {
ignoreReact18DoubleRender.current = false;
}
};
// ******************
// Events
// ******************
const handleMountEvents = () => {
addCacheDataListener(request);
addLifecycleListeners(request);
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.onInvalidate(cacheKey, handleFetch);
const unmount = () => {
clearCacheDataListener();
focusUnmount();
blurUnmount();
onlineUnmount();
invalidateUnmount();
};
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;
},
get success() {
setRenderKey("success");
return state.success;
},
get extra() {
setRenderKey("extra");
return state.extra;
},
get retries() {
setRenderKey("retries");
return state.retries;
},
get timestamp() {
setRenderKey("timestamp");
return state.timestamp;
},
bounce: getBounceData(bounceData),
...actions,
...callbacks,
refetch,
};
};