Stuart Thomson

Stuart Thomson

💡

Update (14 Jan 2023): The React team’s intended solution has been released in canary builds for a while: the use hook.

All the articles I've been able to find so far will introduce Suspense and a bit of what it does. In the meat of the articles they'll say "Use React.lazy() for code splitting" or "React 18 has a useTransition() hook for concurrent features", but don't actually elaborate on how Suspense works (or at least how React.lazy and the existing libraries trigger Suspense).

I am not the first to go down this rabbit hole, but I wasn't able to get the answers I wanted easily with a couple of searches. It turns out it’s rather simple:

Throw a Promise

When you suspend a component, it throws a Promise. It took me a moment to realise how interesting of an idea this is. Regular Javascript code has a single mechanism for exiting a function: returning. Except that’s not quite true. throwing is another mechanism for exiting, but instead of resuming execution after the function call, it instead bubbles up to the nearest catch block; effectively acting as a failure state. But it doesn’t have to be a failure state. React is using the exiting feature of throwing to return to a different area of code. It’s like GOTO for the modern era, and much like GOTO it probably shouldn’t be used in this way.

I’ve written a quick demo app to demonstrate this. When you click the button, the render of Suspendable will throw a Promise that resolves in two seconds. During those two seconds, you see the fallback of the Suspense instead of the button. It’s a small and simple example, though it is utterly useless in a real application. For that, you probably want to take the time to decipher the React team's example.

tsx
function Suspendable() {
const [isSuspended, setIsSuspended] = useState(false);
 
if (isSuspended) {
throw new Promise<void>((resolve) => {
setTimeout(() => {
setIsSuspended(false);
resolve();
}, 2000);
});
}
 
return (
<button onClick={() => setIsSuspended(true)}>
Click to suspend
</button>
);
}
 
function App() {
return (
<Suspense fallback={<p>Fallback</p>}>
<Suspendable />
</Suspense>
);
}

So, should you be throwing Promises in your own apps? No. In most languages, the only thing you can throw is an exception of some sort (or equivalent, like Java's Throwable), but in Javascript you can throw anything; even undefined. Generally, throwing non-Error values is ill-advised, and I'd recommend using the no-throw-literal rule provided by eslint or typescript-eslint for everyday code.

The use hook

The latest development of this story is the React team including a new hook in canary builds of React. Initially proposed in late 2022, use is a new hook with new rules around it. Pass it a Promise, and your component suspends until that Promise resolves.

So how does the use hook work if it’s not throwing a Promise? It throws a custom Error instead, and that error contains a Promise. Unlike the Promise constructor, which is globally available, this Error is only available inside React. Doing this means that developers aren’t able to trigger these conditions themselves. use already integrates right into the Suspense lifecycle, handling all of the logic that you’d just need to re-implement yourself anyway.

Wait for use. Don’t throw Promises, and don’t switch to canary builds just for this. Canary builds are, by their very nature, unstable. If you’re finding yourself in a position where you want to trigger Suspense manually, maybe find some other approach until the official solution is released.