Suspense
Suspend rendering while loading asynchronous data or code. Display a fallback UI until the suspended content is ready.
Suspense Component
Signature
interface SuspenseProps {
children?: ComponentChildren;
fallback?: ComponentChildren;
}
class Suspense extends Component<SuspenseProps> {}
The content to render once data is loaded.
The fallback UI to show while suspended (typically a loading indicator).
Usage
import { Suspense } from 'preact/compat';
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<UserProfile />
</Suspense>
);
}
lazy
Lazy load components for code splitting.
Signature
function lazy<T extends ComponentType<any>>(
loader: () => Promise<{ default: T }>
): T
loader
() => Promise<{ default: T }>
required
A function that returns a Promise resolving to a module with a default export containing the component.
A lazy-loaded component that can be rendered like a normal component.
Usage
import { Suspense, lazy } from 'preact/compat';
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function App() {
return (
<Suspense fallback={<div>Loading component...</div>}>
<HeavyComponent />
</Suspense>
);
}
Complete Examples
Lazy Loading Routes
import { Suspense, lazy } from 'preact/compat';
import { Router, Route } from 'preact-router';
const Home = lazy(() => import('./routes/Home'));
const About = lazy(() => import('./routes/About'));
const Dashboard = lazy(() => import('./routes/Dashboard'));
function App() {
return (
<Suspense fallback={<div className="loader">Loading...</div>}>
<Router>
<Route path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/dashboard" component={Dashboard} />
</Router>
</Suspense>
);
}
Multiple Lazy Components
import { Suspense, lazy } from 'preact/compat';
const Sidebar = lazy(() => import('./Sidebar'));
const Content = lazy(() => import('./Content'));
function Dashboard() {
return (
<div className="dashboard">
<Suspense fallback={<div>Loading sidebar...</div>}>
<Sidebar />
</Suspense>
<Suspense fallback={<div>Loading content...</div>}>
<Content />
</Suspense>
</div>
);
}
Nested Suspense
import { Suspense, lazy } from 'preact/compat';
const Comments = lazy(() => import('./Comments'));
const UserProfile = lazy(() => import('./UserProfile'));
function Post() {
return (
<Suspense fallback={<PageLoader />}>
<article>
<h1>Blog Post</h1>
<Suspense fallback={<ProfileSkeleton />}>
<UserProfile />
</Suspense>
<p>Post content...</p>
<Suspense fallback={<CommentsSkeleton />}>
<Comments />
</Suspense>
</article>
</Suspense>
);
}
Data Fetching with Suspense
import { Suspense } from 'preact/compat';
// Create a resource that suspends
function fetchUser(userId) {
let status = 'pending';
let result;
const promise = fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
status = 'success';
result = data;
})
.catch(error => {
status = 'error';
result = error;
});
return {
read() {
if (status === 'pending') throw promise;
if (status === 'error') throw result;
return result;
}
};
}
const userResource = fetchUser(1);
function UserProfile() {
const user = userResource.read();
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
function App() {
return (
<Suspense fallback={<div>Loading user...</div>}>
<UserProfile />
</Suspense>
);
}
Implementation Details
Suspense Component
The Suspense implementation:
export function Suspense() {
this._pendingSuspensionCount = 0;
this._suspenders = null;
this._detachOnNextRender = null;
}
Suspense.prototype = new Component();
Suspense.prototype.render = function (props, state) {
if (this._detachOnNextRender) {
// Handle detaching suspended content
if (this._vnode._children) {
const detachedParent = document.createElement('div');
const detachedComponent = this._vnode._children[0]._component;
this._vnode._children[0] = detachedClone(
this._detachOnNextRender,
detachedParent,
(detachedComponent._originalParentDom = detachedComponent._parentDom)
);
}
this._detachOnNextRender = null;
}
return [
createElement(Fragment, null, state._suspended ? null : props.children),
state._suspended && createElement(Fragment, null, props.fallback)
];
};
lazy Function
The lazy implementation:
export function lazy(loader) {
let prom;
let component = null;
let error;
let resolved;
function Lazy(props) {
if (!prom) {
prom = loader();
prom.then(
exports => {
if (exports) {
component = exports.default || exports;
}
resolved = true;
},
e => {
error = e;
resolved = true;
}
);
}
if (error) {
throw error;
}
if (!resolved) {
throw prom;
}
return component ? createElement(component, props) : null;
}
Lazy.displayName = 'Lazy';
return Lazy;
}
How Suspense Works
- Promise Thrown: A component throws a Promise when data isn’t ready
- Suspense Catches: Nearest
Suspense boundary catches the Promise
- Fallback Shown:
Suspense renders the fallback prop
- Promise Resolves: When the Promise resolves, the component re-renders
- Content Displayed: Real content replaces the fallback
Error Boundaries with Suspense
Combine with error boundaries for error handling:
import { Component, Suspense, lazy } from 'preact/compat';
class ErrorBoundary extends Component {
state = { hasError: false };
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error('Error:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return <div>Something went wrong.</div>;
}
return this.props.children;
}
}
const LazyComponent = lazy(() => import('./Component'));
function App() {
return (
<ErrorBoundary>
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
</ErrorBoundary>
);
}
Fallback Components
Simple Loader
function Loader() {
return (
<div className="loader">
<div className="spinner" />
<p>Loading...</p>
</div>
);
}
<Suspense fallback={<Loader />}>
<Content />
</Suspense>
Skeleton Screen
function UserSkeleton() {
return (
<div className="skeleton">
<div className="skeleton-avatar" />
<div className="skeleton-line" />
<div className="skeleton-line short" />
</div>
);
}
<Suspense fallback={<UserSkeleton />}>
<UserProfile />
</Suspense>
Best Practices
- Granular Suspense: Use multiple
Suspense boundaries for better UX
- Meaningful Fallbacks: Show skeleton screens instead of generic spinners
- Error Boundaries: Always wrap
Suspense in error boundaries
- Loading States: Consider showing partial content while loading
- Code Splitting: Use
lazy for route-based code splitting
Transition to Loaded Content
Use CSS for smooth transitions:
.content {
animation: fadeIn 0.3s ease-in;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
<Suspense fallback={<Loader />}>
<div className="content">
<UserProfile />
</div>
</Suspense>
useTransition Hook
For smoother transitions between loading states:
import { useState, useTransition, Suspense, lazy } from 'preact/compat';
const Tab1 = lazy(() => import('./Tab1'));
const Tab2 = lazy(() => import('./Tab2'));
function Tabs() {
const [tab, setTab] = useState('tab1');
const [isPending, startTransition] = useTransition();
const switchTab = (newTab) => {
startTransition(() => {
setTab(newTab);
});
};
return (
<>
<button onClick={() => switchTab('tab1')}>Tab 1</button>
<button onClick={() => switchTab('tab2')}>Tab 2</button>
{isPending && <div>Loading tab...</div>}
<Suspense fallback={<div>Loading...</div>}>
{tab === 'tab1' ? <Tab1 /> : <Tab2 />}
</Suspense>
</>
);
}
Source
Implementation: compat/src/suspense.js:1-255