@hyper-fetch/react
Version:
React hooks and utils for the hyper-fetch
261 lines (235 loc) • 8.19 kB
text/typescript
import { useMemo, useRef } from "react";
import {
Request,
getRequestKey,
ExtractAdapterReturnType,
RequestInstance,
sendRequest,
ResponseReturnType,
ExtractResponseType,
ExtractErrorType,
RequestSendOptionsType,
RequestSendType,
ExtractAdapterType,
} from "@hyper-fetch/core";
import { useDidMount } from "@better-hooks/lifecycle";
import { useDebounce, useThrottle } from "@better-hooks/performance";
import { UseSubmitOptionsType, useSubmitDefaultOptions, UseSubmitReturnType } from "hooks/use-submit";
import { useTrackedState, useRequestEvents } from "helpers";
import { useConfigProvider } from "config-provider";
import { getBounceData } from "utils";
import { InvalidationKeyType } from "types";
/**
* This hooks aims to mutate data on the server.
* @param request
* @param options
* @returns
*/
export const useSubmit = <RequestType extends RequestInstance>(
request: RequestType,
options: UseSubmitOptionsType<RequestType> = useSubmitDefaultOptions,
): UseSubmitReturnType<RequestType> => {
// Build the configuration options
const [globalConfig] = useConfigProvider();
const mergedOptions: UseSubmitOptionsType<RequestType> = useMemo(
() => ({
...useSubmitDefaultOptions,
...globalConfig.useSubmitConfig,
...options,
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[globalConfig.useSubmitConfig, JSON.stringify(options), options.deepCompare],
);
const { disabled, dependencyTracking, initialData, bounce, bounceType, bounceTime, deepCompare } = mergedOptions;
/**
* Because of the dynamic cacheKey / queueKey signing within the request we need to store it's latest instance
* so the events got triggered properly and show the latest result without mixing it up
*/
const { client } = request;
const { cache, submitDispatcher: dispatcher, loggerManager } = client;
const logger = useRef(loggerManager.init("useSubmit")).current;
const requestDebounce = useDebounce({ delay: bounceTime });
const requestThrottle = useThrottle({
interval: bounceTime,
timeout: "bounceTimeout" in mergedOptions ? mergedOptions.bounceTimeout : bounceTime,
});
const bounceResolver = useRef<
(
value: ResponseReturnType<
ExtractResponseType<RequestType>,
ExtractErrorType<RequestType>,
ExtractAdapterType<RequestType>
>,
) => void
>(() => null);
const bounceData = bounceType === "throttle" ? requestThrottle : requestDebounce;
const bounceFunction = bounceType === "throttle" ? requestThrottle.throttle : requestDebounce.debounce;
/**
* State handler with optimization for rerendering, that hooks into the cache state and dispatchers queues
*/
const [state, actions, { setRenderKey, setCacheData, getIsDataProcessing }] = useTrackedState<RequestType>({
logger,
request,
dispatcher,
initialData,
deepCompare,
dependencyTracking,
});
/**
* 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 } = listeners;
// ******************
// Submitting
// ******************
const handleSubmit: RequestSendType<RequestType> = (submitOptions?: RequestSendOptionsType<RequestType>) => {
const requestClone = request.clone(submitOptions as any) as RequestType;
if (disabled) {
logger.warning(`Cannot submit request`, { disabled, submitOptions });
return Promise.resolve({
data: null,
error: new Error("Cannot submit request. Option 'disabled' is enabled"),
status: null,
extra: request.client.defaultExtra,
}) as Promise<
ResponseReturnType<
ExtractResponseType<RequestType>,
ExtractErrorType<RequestType>,
ExtractAdapterType<RequestType>
>
>;
}
const triggerRequest = () => {
addCacheDataListener(requestClone);
return sendRequest(requestClone, {
dispatcherType: "submit",
...submitOptions,
onSettle: (requestId, cmd) => {
addLifecycleListeners(requestClone, requestId);
submitOptions?.onSettle?.(requestId, cmd);
},
});
};
return new Promise<ExtractAdapterReturnType<RequestType>>((resolve) => {
const performSubmit = async () => {
logger.debug(`Submitting request`, { disabled, submitOptions });
if (bounce) {
const bouncedResolve = bounceResolver.current;
// We need to keep the resolve of debounced requests to prevent memory leaks - we need to always resolve promise.
// By default bounce method will prevent function to be triggered, but returned promise will still await to be resolved.
// This way we can close previous promise, making sure our logic will not stuck in memory.
bounceResolver.current = (
value: ResponseReturnType<
ExtractResponseType<RequestType>,
ExtractErrorType<RequestType>,
ExtractAdapterType<RequestType>
>,
) => {
// Trigger previous awaiting calls to resolve together in bounced batches
bouncedResolve(value);
resolve(value);
};
// Start bounce
bounceFunction(async () => {
// We will always resolve previous calls as we stack the callbacks together until bounce function trigger
const callback = bounceResolver.current;
// Clean bounce resolvers to start the new stack
bounceResolver.current = () => null;
const value = await triggerRequest();
callback(value);
});
} else {
const value = await triggerRequest();
resolve(value);
}
};
performSubmit();
});
};
// ******************
// Invalidation
// ******************
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) return;
if (invalidateKey && Array.isArray(invalidateKey)) {
invalidateKey.forEach(handleInvalidation);
} else if (invalidateKey && !Array.isArray(invalidateKey)) {
handleInvalidation(invalidateKey);
}
};
// ******************
// Misc
// ******************
const handlers = {
onSubmitSuccess: callbacks.onSuccess,
onSubmitError: callbacks.onError,
onSubmitFinished: callbacks.onFinished,
onSubmitRequestStart: callbacks.onRequestStart,
onSubmitResponseStart: callbacks.onResponseStart,
onSubmitDownloadProgress: callbacks.onDownloadProgress,
onSubmitUploadProgress: callbacks.onUploadProgress,
onSubmitOfflineError: callbacks.onOfflineError,
onSubmitAbort: callbacks.onAbort,
};
// ******************
// Lifecycle
// ******************
useDidMount(() => {
addCacheDataListener(request);
});
return {
submit: handleSubmit,
get data() {
setRenderKey("data");
return state.data;
},
get error() {
setRenderKey("error");
return state.error;
},
get submitting() {
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;
},
abort: callbacks.abort,
...actions,
...handlers,
bounce: getBounceData(bounceData),
refetch,
};
};