|
@@ -1,6 +1,8 @@
|
|
|
-import { useState, useEffect, useCallback } from 'react';
|
|
|
+import { useState, useEffect, useRef } from 'react';
|
|
|
+import { normalizeError } from '../../functions/misc';
|
|
|
+import { randomBytes } from 'crypto';
|
|
|
|
|
|
-export type UsePromiseReturnValues<T> = [T, any, boolean, () => Promise<void|null>];
|
|
|
+export type UsePromiseReturnValues<T> = [T, string | null, boolean];
|
|
|
|
|
|
export default function usePromise<T> (
|
|
|
promise: () => Promise<T>,
|
|
@@ -10,44 +12,58 @@ export default function usePromise<T> (
|
|
|
): UsePromiseReturnValues<T> {
|
|
|
const [state, setState] = useState<{
|
|
|
value: T;
|
|
|
- error: any;
|
|
|
+ error: string | null;
|
|
|
isPending: boolean;
|
|
|
}>({ value: defaultValue, error: null, isPending: true });
|
|
|
|
|
|
- let isSubscribed = true;
|
|
|
- const execute = useCallback(() => {
|
|
|
- setState({ value: state.value, error: null, isPending: true });
|
|
|
+ const subscribedPromiseToken = useRef<string | null>(null);
|
|
|
|
|
|
- return promise()
|
|
|
+ const executeAndSubscribePromise = () => {
|
|
|
+ setState((state) => ({ ...state, error: null, isPending: true }));
|
|
|
+
|
|
|
+ const thisPromiseToken = randomBytes(32).toString('hex');
|
|
|
+
|
|
|
+ subscribedPromiseToken.current = thisPromiseToken;
|
|
|
+
|
|
|
+ promise()
|
|
|
.then((value) => {
|
|
|
- if (isSubscribed) {
|
|
|
+ if (subscribedPromiseToken.current === thisPromiseToken) {
|
|
|
setState({ value, error: null, isPending: false });
|
|
|
|
|
|
if (onUpdate) {
|
|
|
onUpdate(value);
|
|
|
}
|
|
|
+ } else {
|
|
|
+ console.warn('usePromise: Token didn\'t match on .then()');
|
|
|
+ console.warn(`Subscribed promise: ${subscribedPromiseToken.current || 'NONE'}. Resolved promise: ${thisPromiseToken}.`);
|
|
|
}
|
|
|
})
|
|
|
.catch((error) => {
|
|
|
- if (isSubscribed) {
|
|
|
- setState({ value: defaultValue, error: error, isPending: false });
|
|
|
+ if (subscribedPromiseToken.current === thisPromiseToken) {
|
|
|
+ setState({ value: defaultValue, error: normalizeError(error), isPending: false });
|
|
|
|
|
|
if (onUpdate) {
|
|
|
onUpdate(defaultValue); // This should represent an empty value in most cases
|
|
|
}
|
|
|
+ } else {
|
|
|
+ console.warn('usePromise: Token didn\'t match on .catch()');
|
|
|
+ console.warn(`Subscribed promise: ${subscribedPromiseToken.current || 'NONE'}. Rejected promise: ${thisPromiseToken}.`);
|
|
|
}
|
|
|
});
|
|
|
- }, [promise]);
|
|
|
+ };
|
|
|
|
|
|
useEffect(() => {
|
|
|
- execute();
|
|
|
+ executeAndSubscribePromise();
|
|
|
|
|
|
return () => {
|
|
|
- isSubscribed = false;
|
|
|
+ subscribedPromiseToken.current = null;
|
|
|
};
|
|
|
+ // Silence "React Hook useEffect was passed a dependency list that is not an array literal",
|
|
|
+ // since we want to preserve the ability to pass custom "depnendencies".
|
|
|
+ // eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
}, dependsOn);
|
|
|
|
|
|
const { value, error, isPending } = state;
|
|
|
|
|
|
- return [value, error, isPending, execute];
|
|
|
+ return [value, error, isPending];
|
|
|
}
|