I have this to check if my server is alive, polling every 10 seconds for a max of 10 times to see if it's alive.
const wait = (ms: number) => new Promise((res) => setTimeout(res, ms));
export async function checkRemote(
{ maxAttempts = 10 }: { maxAttempts: number } = { maxAttempts: 10 }
) {
let i = 1;
while (true) {
const res = await fetch(`/check`);
if (res.status >= 400) {
if (i === maxAttempts) {
throw new Error("Check failed");
} else {
i++;
await wait(10000);
continue;
}
} else {
const json = await res.json();
return json;
}
}
}
How can I properly "cancel" this polling in the context of React.js?
The problem is, my checkRemote function is going to continue to execute after my React component unmounts... If it unmounts while the calls are still ongoing.
First of all, the call to checkRemote must be made in a useEffect hook, not as the result of a button click? Or can we make it occur on button click (that would be ideal IMO), but still make it cancelable if they unmount the current component?
useEffect(() => {
const promise = checkRemote({ maxAttempts: 10 })
.then(res => {
setStatus('ready')
}).catch(e => {
setStatus('unvailable')
})
return () => {
// on unmount, do something like this?
promise.abort()
}
})
Or perhaps for button click:
const [checkPromise, setCheckPromise] = useState<Promise>()
const handleClick = () => {
const promise = checkRemote({ maxAttempts: 10 })
.then((res) => {
setStatus("ready");
})
.catch((e) => {
setStatus("unvailable");
});
setCheckPromise(promise)
}
useEffect(() => {
return () => checkPromise.abort()
}, [checkPromise])
return <button onClick={handleClick}>Click me</button>
How could I architect this to pass in the "promise abort handler" into my nested functions? Something like AbortController....
const wait = (ms: number, { controller }) => {
return new Promise((res) => {
let timer = setTimeout(res, ms)
controller.on('abort', () => {
clearTimeout(timer)
// to respond or not respond then?
res()
})
})
}
export async function checkRemote(
{ maxAttempts = 10, controller }: { maxAttempts: number } = {
maxAttempts: 10,
}
) {
let i = 1;
while (!controller.aborted) {
const res = await fetch(`/check`, { signal: controller });
if (res.status >= 400) {
if (i === maxAttempts) {
throw new Error("Check failed");
} else {
i++;
// somehow stop the timer early if we abort
await wait(10000, { controller });
continue;
}
} else {
const json = await res.json();
return json;
}
}
}
If I go down this rabbit hole, what are your recommendations? How do I architect the system to abort every function "properly"? I would probably use a custom event emitter to do this I'm guessing, so I can have controller.on('abort') easily throughout.
What I don't want to happen is my 10 second * 10 attempt = ~2 minute checker continues to check when the component is unmounted, that would be painful and confusing. I would like for it to cancel everything on unmount basically.
Note: the general problem is how to setup async aborting, not to useContext or something for this specific use case.
AbortSignal, not theAbortController(and name your parameters accordingly), but otherwise yes that's the way to gosetTimeoutinwait, see here