In an attempt to understand react's <Suspense> component, I'm trying to utilize setTimeout to write a simple hook to trigger a "suspended" state for some amount of time before rendering.
In my testing I'm using the following setup:
import { Suspense, useMemo, useState } from "react";
function useSuspend(ms: number) {
const [isSuspended, setSuspended] = useState(true);
const promise = useMemo(
() =>
new Promise<void>((resolve) => {
setTimeout(() => {
setSuspended(false);
resolve();
}, ms);
}),
[ms]
);
console.table({ isSuspended, ms });
if (isSuspended) {
throw promise;
}
}
function Suspending() {
const [id] = useState(Math.random().toFixed(20));
console.log(id);
useSuspend(2500);
return "Done";
}
export default function Main() {
return (
<Suspense fallback={"Loading..."}>
<Suspending />
</Suspense>
);
}
However, this produces some (to me) rather unexpected log prints:
0.91830134558829579206
┌─────────────┬────────┐
│ (index) │ Values │
├─────────────┼────────┤
│ isSuspended │ true │
│ ms │ 2500 │
└─────────────┴────────┘
(2.5s pause)
0.33716150767738661820
┌─────────────┬────────┐
│ (index) │ Values │
├─────────────┼────────┤
│ isSuspended │ true │
│ ms │ 2500 │
└─────────────┴────────┘
(continues infinitely every 2.5s forever)
The text "Done" is never rendered either.
These logs would seem to indicate that the <Suspending /> component does not retain its state after the useSuspend hook completes, prompting the component to render "as if new", which is counter intuitive to me. Could someone please explain this behavior?
That's correct. Suspense is designed so that when it catches a promise it will unmount the child tree and render the fallback (if any) instead. Later, the promise resolves and suspense will once again mount the children. As is typical for newly mounted components, their states will be assigned their initial values (ie,
isSuspendedistrue), and theuseMemoruns and creates a new promise. This promise then gets thrown, and the process repeatsThrowing promises for suspense to catch is kinda tricky. You typically need to have some value that exists outside the component which you can synchronously check to see if the asynchronous work has been done. If it hasn't, you then throw a promise which can set that external value and resolve itself. For example:
Note that since
loadedis a global variable, every component in your app is sharing it. Whichever component callsuseSuspendfirst will start a timeout, and all others will reuse the same promise. If you want different components to have different timeouts, you'll need to create some more complex store of values which match your needs.