Using Promises in React with hooks or with classes isn't as easy as it might seem at first. Let's look at a simple example to illustrate the problem:
const [result, setResult] = useState<string | undefined>(undefined)
useEffect(() => {
promiseReturningFunction(a).then(res => setResult(res))
}, [a])
This code might not do what you want it to do - what's the problem with this implementation?
1.
Let's suppose that a
is 1
at first, the request is sent and takes 1000ms, but a
is changed immediately to 2
, therefore another request is sent and that one could return before the first one. Therefore the first request which returns after the second one and will override the value that is associated with 2
. That would result in the result of the a = 1
request being displayed although a
currently is 2
.
a = 1 a = 2 setResult(2) setResult(1) result = 1, a = 2 ?!?
| \----------/ |
\--------------------------------/
2.
There is also another error which you might experience when using a dev build of react: A state update on an unmounted component (It's also a problem in if you are using a prod build of react but it won't notify you). When the component is unmounted while a promise is still pending the .then
will call setResult
although the component is no longer mounted:
request: |------| setResult
component: |------| unmounted
The solution is quite simple: We have to "cancel" the request when the effect is supposed to do it's cleanup. Ok how can we achieve that? useRef
to store the promise - sadly not because promises cannot be cancelled. What about a useRef
to store a boolean variable called cancelled
? Better but that will only handle the second problem. A simple variable scoped to the effect function will do the trick:
const [result, setResult] = useState<string | undefined>(undefined)
useEffect(() => {
let cancel = false;
promiseReturningFunction(a).then(res => {
if (cancel) return;
setResult(res)
})
return () => {
cancel = true;
}
}, [a])
Okay but that seems like a lot of code to write every time you want to consume some async function, it might be a better idea to extract this logic into a custom hook - let's call it useAsync
.
Let's think about the parameters that such a hook could have:
-
fn: () => Promise<T>
(the function to call) -
deps: any[]
(the deps of useEffect)
const useAsync = <T>(fn: () => Promise<T>, deps: any[]) => {
const [res, setRes] = useState<T | undefined>();
useEffect(() => {
let cancel = false;
fn().then(res => {
if (cancel) return;
setRes(res)
})
return () => {
cancel = true;
}
}, deps)
return res;
}
Usage
const result = useAsync(() => fn(a), [a])
But it seems like at least two things are missing: a loading state and error handling - let's add them:
const useAsync = <T>(fn: () => Promise<T>, deps: any[]) => {
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<Error | undefined>();
const [res, setRes] = useState<T | undefined>();
useEffect(() => {
setLoading(true);
let cancel = false;
fn().then(res => {
if (cancel) return;
setLoading(false);
setRes(res)
}, error => {
if (cancel) return;
setLoading(false);
setError(error);
})
return () => {
cancel = true;
}
}, deps)
return {loading, error, res};
}
The problem here is not just limited to hooks. Class components in React have the same problem, but it mostly is ignored. It example shows that hooks are great for generically describing behavior without a lot of copy-pasting.
Top comments (0)