Suspense
Suspense demo built with Expressive MVC - the complete source below runs as an editable sandbox when JavaScript is enabled.
App.tsx
import './App.css';
import { Suspense } from 'react';
import State, { Component, Provider, set } from '@expressive/react';
// An async `set` factory suspends on read and resolves straight into the
// field - no `waiting` flag, no `error` state threaded by hand. Compare
// essentials/fetch, which tracks that same lifecycle manually.
class Greeter extends State {
hello = set(async () => {
const res = await fetch('https://randomuser.me/api?nat=us&results=1');
if (!res.ok) throw new Error(`Server responded ${res.status}.`);
const { first, last } = (await res.json()).results[0].name;
return `Hello, ${first} ${last}.`;
});
}
// Reading `hello` while pending throws the suspense signal <Suspense>
// catches; a rejected fetch throws past it to the boundary's catch().
function Hello() {
return <p className="result">{Greeter.get().hello}</p>;
}
// The Greeter is provided *above* <Suspense> so it survives the suspended
// renders - an instance created inside the boundary would be rebuilt (and
// refetch) on every retry. Bumping `round` remounts it for a fresh request.
export default class Demo extends Component {
round = 0;
resume = () => {};
catch(error: Error) {
this.fallback = (
<div className="status error">
<p>{error.message}</p>
<button onClick={() => this.again()}>Try again</button>
</div>
);
return new Promise<void>((resolve) => (this.resume = resolve));
}
again() {
this.round++;
this.resume();
}
render() {
return (
<div className="container">
<h1>Suspense</h1>
<p>
The greeting is an async <code>set</code> factory. Reading it
suspends until the fetch resolves — no waiting flag, no error field.
</p>
<Provider for={Greeter} key={this.round}>
<Suspense fallback={<p className="status">Contacting server…</p>}>
<Hello />
</Suspense>
</Provider>
<button onClick={() => this.again()}>Say hello again</button>
</div>
);
}
}
App.css
.result {
font-size: var(--t-lg);
color: var(--fg);
}
.status {
color: var(--muted);
}
.status.error {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--s3);
color: var(--fg);
}