mod-arch-shared
Version:
Shared library for modular architecture micro-frontend projects
153 lines • 5.75 kB
JavaScript
import * as React from 'react';
/**
* Allows "I'm not ready" rejections if you lack a lazy provided prop
* e.g. Promise.reject(new NotReadyError('Do not have namespace'))
*/
export class NotReadyError extends Error {
constructor(reason) {
super(`Not ready yet. ${reason}`);
this.name = 'NotReadyError';
}
}
/**
* Checks to see if it's a standard error handled by useStateFetch .catch block.
*/
export const isCommonStateError = (e) => {
if (e.name === 'NotReadyError') {
// An escape hatch for callers to reject the call at this fetchCallbackPromise reference
// Re-compute your callback to re-trigger again
return true;
}
if (e.name === 'AbortError') {
// Abort errors are silent
return true;
}
return false;
};
const isAdHocUpdate = (r) => typeof r === 'function';
/**
* A boilerplate helper utility. Given a callback that returns a promise, it will store state and
* handle refreshes on intervals as needed.
*
* Note: Your callback *should* support the opts property so the call can be cancelled.
*/
export const useFetchState = (
/** React.useCallback result. */
fetchCallbackPromise,
/**
* A preferred default states - this is ignored after the first render
* Note: This is only read as initial value; changes do nothing.
*/
initialDefaultState,
/** Configurable features */
{ refreshRate = 0, initialPromisePurity = false } = {}) => {
const initialDefaultStateRef = React.useRef(initialDefaultState);
const [result, setResult] = React.useState(initialDefaultState);
const [loaded, setLoaded] = React.useState(false);
const [loadError, setLoadError] = React.useState(undefined);
const abortCallbackRef = React.useRef(() => undefined);
const changePendingRef = React.useRef(true);
/** Setup on initial hook a singular reset function. DefaultState & resetDataOnNewPromise are initial render states. */
const cleanupRef = React.useRef(() => {
if (initialPromisePurity) {
setResult(initialDefaultState);
setLoaded(false);
setLoadError(undefined);
}
});
React.useEffect(() => {
cleanupRef.current();
}, [fetchCallbackPromise]);
const call = React.useCallback(() => {
let alreadyAborted = false;
const abortController = new AbortController();
/** Note: this promise cannot "catch" beyond this instance -- unless a runtime error. */
const doRequest = () => fetchCallbackPromise({ signal: abortController.signal })
.then((r) => {
changePendingRef.current = false;
if (alreadyAborted) {
return undefined;
}
if (r === undefined) {
// Undefined is an unacceptable response. If you want "nothing", pass `null` -- this is likely an API issue though.
// eslint-disable-next-line no-console
console.error('useFetchState Error: Got undefined back from a promise. This is likely an error with your call. Preventing setting.');
return undefined;
}
setLoadError(undefined);
if (isAdHocUpdate(r)) {
r((setState) => {
if (alreadyAborted) {
return undefined;
}
setResult(setState);
setLoaded(true);
return undefined;
});
return undefined;
}
setResult(r);
setLoaded(true);
return r;
})
.catch((e) => {
changePendingRef.current = false;
if (alreadyAborted) {
return undefined;
}
if (isCommonStateError(e)) {
return undefined;
}
setLoadError(e);
return undefined;
});
const unload = () => {
changePendingRef.current = false;
if (alreadyAborted) {
return;
}
alreadyAborted = true;
abortController.abort();
};
return [doRequest(), unload];
}, [fetchCallbackPromise]);
// Use a memmo to update the `changePendingRef` immediately on change.
React.useMemo(() => {
changePendingRef.current = true;
// React to changes to the `call` reference.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [call]);
React.useEffect(() => {
let interval;
const callAndSave = () => {
const [, unload] = call();
abortCallbackRef.current = unload;
};
callAndSave();
if (refreshRate > 0) {
interval = setInterval(() => {
abortCallbackRef.current();
callAndSave();
}, refreshRate);
}
return () => {
clearInterval(interval);
abortCallbackRef.current();
};
}, [call, refreshRate]);
// Use a reference for `call` to ensure a stable reference to `refresh` is always returned
const callRef = React.useRef(call);
callRef.current = call;
const refresh = React.useCallback(() => {
abortCallbackRef.current();
const [callPromise, unload] = callRef.current();
abortCallbackRef.current = unload;
return callPromise;
}, []);
// Return the default reset state if a change is pending and initialPromisePurity is true
if (initialPromisePurity && changePendingRef.current) {
return [initialDefaultStateRef.current, false, undefined, refresh];
}
return [result, loaded, loadError, refresh];
};
//# sourceMappingURL=useFetchState.js.map