Reputation: 3534
I have written a convenience hook for React that tracks whether a promise is running, whether there is an error, and what the results are. It's used like this:
const MyComponent = (props: IProps) => {
const [promise, setPromise} = useState<Promise | undefined>();
const {
hasRun,
isWaiting,
results,
error
} = useService(promise);
const doSomething = () => {
setPromise(runSomeAsyncCode());
};
return (
<div>...</div>
);
};
It's not much more than a group of states that are updated as the promise starts, runs, succeeds, or fails:
export const useService = <T>(promise?: Promise<T>) => {
const [hasRun, setHasRun] = useState<boolean>(false);
const [isWaiting, setIsWaiting] = useState<boolean>(false);
const [error, setError] = useState<Error | undefined>(undefined);
const [results, setResults] = useState<T | undefined>();
useEffect(() => {
if (!promise) {
return;
}
(async () => {
if (!hasRun) {
setHasRun(true);
}
setError(undefined);
setIsWaiting(true);
try {
const r = await promise;
setResults(r);
} catch (err) {
setResults(undefined);
setError(err);
} finally {
setIsWaiting(false);
}
})();
}, [promise]);
return {
error,
hasRun,
isWaiting,
results,
};
};
My problem is if the promise updates before a previous promise resolves, then the previous promise's results or error will still be returned to the component. For example, launching a couple AJAX requests, where the first fails after a minute but the second resolves in a couple seconds, leads to an initial success but then a reported failure a minute later.
How can I modify the hook so that it doesn't call setState for error or success if the promise has changed in the meantime?
Codepen: https://codepen.io/whiterook6/pen/gOPwJGq
Upvotes: 0
Views: 2198
Reputation: 2679
Why not keep track of the current promise
, and bail the effect if the promise has changed?
export const useService = <T>(promise?: Promise<T>) => {
const [hasRun, setHasRun] = useState<boolean>(false);
const [isWaiting, setIsWaiting] = useState<boolean>(false);
const [error, setError] = useState<Error | undefined>(undefined);
const [results, setResults] = useState<T | undefined>();
const promiseRef = useRef(promise);
promiseRef.current = promise; // ** keep ref always up to date
useEffect(() => {
if (!promise) {
return;
}
(async () => {
setHasRun(true);
setError(undefined);
setIsWaiting(true);
try {
const r = await promise;
if (promiseRef.current !== promise) {
return;
}
setResults(r);
setIsWaiting(false);
} catch (err) {
if (promiseRef.current !== promise) {
return;
}
setResults(undefined);
setError(err);
setIsWaiting(false);
}
})();
// you may want to reset states when the promise changes
return () => {
setHasRun(false);
setIsWaiting(false);
setError(undefined);
setResults(undefined);
}
}, [promise]);
return {
error,
hasRun,
isWaiting,
results,
};
};
useRef
isn't just for DOM element refs, as the docs point out.
Essentially, useRef is like a “box” that can hold a mutable value in its .current property. [...] Mutating the .current property doesn’t cause a re-render.
The reason I used useRef
here is because we need a mutable value that can hold the latest promise
argument without causing a rerender. Because promiseRef
never changes (only .current
does), you can assign the latest value on the line with **
and access it in the async function, even after time has passed and the component has rerendered.
Upvotes: 2