Suspense
Polyfill of the React <Suspense> component.
Renders children normally. When a descendant component suspends by throwing a Promise (the standard Suspense contract used by React.lazy, data-fetching libraries such as SWR, React Query, and Relay, and the use() hook), fallback is rendered in its place until the Promise resolves, at which point children are rendered again.
How it works
The polyfill relies on the same mechanism used by ErrorBoundary: getDerivedStateFromError is called synchronously when any descendant throws during rendering. If the thrown value is a Promise, the boundary marks itself as suspended and renders fallback. Once the Promise settles (resolved or rejected), the boundary resets and React re-renders children.
Differences from native React <Suspense>
- No concurrent rendering: the native
<Suspense>integrates with React's concurrent mode to render suspended subtrees in the background without blocking the UI. This polyfill performs a synchronoussetStateon Promise resolution, which may cause a brief flash of thefallbackeven when the Promise resolves immediately. - No
startTransitionintegration: the native<Suspense>can keep the previous UI visible during a transition while the new subtree loads. This polyfill always showsfallbackimmediately on suspend. - No streaming / server-side rendering support: the native
<Suspense>supports streaming HTML from the server. This polyfill is client-only. - Promise rejection: when a suspended Promise rejects, this polyfill resets the suspended state and re-renders
children, which will throw again — resulting in an infinite loop unless the child handles the error internally. Wrap with anErrorBoundaryto handle rejection gracefully.
See: React Suspense docs
Demo
INFO
The component demonstrates the Suspense polyfill with two scenarios:
- Lazy component: a button triggers the lazy load of a component via
React.lazy. While loading, a spinner fallback is shown. Once ready, the component appears. - Data fetching: a button triggers a simulated async data fetch (1.5s delay) using the Suspense throw-Promise contract. While fetching, a skeleton fallback is shown. Once resolved, the data is displayed. Both scenarios show the
fallbackprop in action and demonstrate thatSuspensecatches thrown Promises from descendant components.
Show source code
import { useState } from "react";
import { Suspense } from "../../..";
// ---------------------------------------------------------------------------
// Lazy component scenario
// ---------------------------------------------------------------------------
const LazyChild = (() => {
let loaded = false;
let promise: Promise<void> | null = null;
return function LazyChild() {
if (!loaded) {
if (!promise) {
promise = new Promise<void>(res => setTimeout(() => { loaded = true; res(); }, 1200));
}
throw promise;
}
return <p style={{ margin: 0, color: "green" }}>✓ Lazy component loaded!</p>;
};
})();
// ---------------------------------------------------------------------------
// Data fetching scenario
// ---------------------------------------------------------------------------
type Status = "idle" | "pending" | "done";
const createResource = (delay: number) => {
let status: Status = "pending";
let result = "";
const promise = new Promise<string>(res =>
setTimeout(() => { result = `Fetched at ${new Date().toLocaleTimeString()}`; status = "done"; res(result); }, delay)
);
return {
read(): string {
if (status === "pending") throw promise;
return result;
},
};
};
type Resource = ReturnType<typeof createResource>;
const DataChild = ({ resource }: { resource: Resource }) => {
const data = resource.read();
return <p style={{ margin: 0, color: "#1e88e5" }}>✓ {data}</p>;
};
// ---------------------------------------------------------------------------
// Demo component
// ---------------------------------------------------------------------------
export default function SuspenseDemo() {
const [showLazy, setShowLazy] = useState(false);
const [resource, setResource] = useState<Resource | null>(null);
return (
<div style={{ display: "grid", gap: 24, maxWidth: 420, margin: "0 auto" }}>
{/* Lazy component */}
<div style={{ border: "1px solid #e0e0e0", borderRadius: 8, padding: 16 }}>
<p style={{ margin: "0 0 12px", fontWeight: "bold" }}>Lazy component</p>
<button onClick={() => setShowLazy(true)} disabled={showLazy}>
Load component
</button>
{showLazy && (
<div style={{ marginTop: 12 }}>
<Suspense fallback={<p style={{ margin: 0, color: "#999" }}>⏳ Loading…</p>}>
<LazyChild />
</Suspense>
</div>
)}
</div>
{/* Data fetching */}
<div style={{ border: "1px solid #e0e0e0", borderRadius: 8, padding: 16 }}>
<p style={{ margin: "0 0 12px", fontWeight: "bold" }}>Data fetching</p>
<button onClick={() => setResource(createResource(1500))}>
{resource ? "Fetch again" : "Fetch data"}
</button>
{resource && (
<div style={{ marginTop: 12 }}>
<Suspense
fallback={
<div style={{ background: "#f5f5f5", borderRadius: 4, height: 20, width: "60%", margin: 0, color: 'black' }}>Fallback</div>
}
>
<DataChild resource={resource} />
</Suspense>
</div>
)}
</div>
</div>
);
}Types
SuspenseState
Internal state managed by the Suspense polyfill.
| Property | Type | Required | Description |
|---|---|---|---|
isSuspended | boolean | ✓ | true when a descendant component has thrown a Promise that has not yet resolved, causing the fallback UI to be rendered in its place. Reset to false once the Promise resolves and the component tree is ready to be rendered. |
suspendedPromise | Promise<unknown> \| null | ✓ | The Promise thrown by a suspended descendant, or null when no component is currently suspended. Used internally to schedule a re-render once the Promise settles. |
SuspenseProps
Props accepted by the Suspense polyfill.
| Property | Type | Required | Description |
|---|---|---|---|
fallback | ReactNode | ✓ | Content rendered while one or more descendant components are suspended (i.e. while a thrown Promise is pending). Accepts any valid React node — typically a spinner, skeleton, or loading message. When the suspended Promise resolves, fallback is replaced by the original children. |
