Portrait of Stuart Thomson

Stuart Thomson

Software Developer | Human Being

Manually suspending a component in React
Manually suspending a component in React

Manually suspending a component in React

Published
Published May 10, 2022
Edited
Tags
#react
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).
So, I went digging around. 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. React's documentation of concurrent mode included a CodeSandbox of what I was looking for, but spent extra time trying to show real use-cases rather than mechanical implementation. Here's what I found:
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. Well, 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, should be used with care.
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? Probably not. 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.
Relying on React internals is a great way to have your app break later down the line. Even though I personally suspect that this method of suspending a component will exist for a long time, it's best to stick with what the React team expose as their public API, such as that useTransition() hook. Better yet, wait for libraries to provide a higher-level way of interacting with these APIs.
Comments (GitHub)