The SuspenseWithBoundary component wraps children with both ErrorBoundary and Suspense, providing a convenient way to handle loading states and errors in a single component.
When to Use
- Handle both loading and error states for async components
- Wrap data-fetching components with unified error/loading handling
- Simplify async component boundaries
- Reduce boilerplate from nested Suspense and ErrorBoundary
Basic Usage
import { SuspenseWithBoundary } from "@zayne-labs/ui-react/common/suspense-with-boundary";
function App() {
return (
<SuspenseWithBoundary
fallback={<div>Loading...</div>}
errorFallback={({ error, resetErrorBoundary }) => (
<div>
<p>Error: {error.message}</p>
<button onClick={resetErrorBoundary}>Retry</button>
</div>
)}
>
<AsyncComponent />
</SuspenseWithBoundary>
);
}
Component API
Components to wrap with Suspense and ErrorBoundary
Loading UI to display while Suspense is triggered
errorFallback
React.ReactNode | ((props: ErrorFallbackProps) => React.ReactNode)
Error UI to display when ErrorBoundary catches an error
Examples
Data Fetching Component
import { SuspenseWithBoundary } from "@zayne-labs/ui-react/common/suspense-with-boundary";
function UserProfile({ userId }) {
return (
<SuspenseWithBoundary
fallback={
<div className="skeleton">
<div className="skeleton-avatar" />
<div className="skeleton-name" />
<div className="skeleton-bio" />
</div>
}
errorFallback={({ error, resetErrorBoundary }) => (
<div className="error-state">
<h3>Failed to load profile</h3>
<p>{error.message}</p>
<button onClick={resetErrorBoundary}>Try Again</button>
</div>
)}
>
<UserData userId={userId} />
</SuspenseWithBoundary>
);
}
Multiple Async Components
import { SuspenseWithBoundary } from "@zayne-labs/ui-react/common/suspense-with-boundary";
function Dashboard() {
return (
<div className="dashboard">
<SuspenseWithBoundary
fallback={<HeaderSkeleton />}
errorFallback={<div>Header failed to load</div>}
>
<Header />
</SuspenseWithBoundary>
<SuspenseWithBoundary
fallback={<ChartSkeleton />}
errorFallback={({ error }) => <div>Chart error: {error.message}</div>}
>
<AnalyticsChart />
</SuspenseWithBoundary>
<SuspenseWithBoundary
fallback={<TableSkeleton />}
errorFallback={<div>Table failed to load</div>}
>
<DataTable />
</SuspenseWithBoundary>
</div>
);
}
Nested Async Content
import { SuspenseWithBoundary } from "@zayne-labs/ui-react/common/suspense-with-boundary";
function ArticlePage({ articleId }) {
return (
<SuspenseWithBoundary
fallback={<PageSkeleton />}
errorFallback={({ error, resetErrorBoundary }) => (
<ErrorPage error={error} onRetry={resetErrorBoundary} />
)}
>
<Article id={articleId}>
<SuspenseWithBoundary
fallback={<CommentsSkeleton />}
errorFallback={<div>Comments unavailable</div>}
>
<Comments articleId={articleId} />
</SuspenseWithBoundary>
</Article>
</SuspenseWithBoundary>
);
}
With React Server Components
import { SuspenseWithBoundary } from "@zayne-labs/ui-react/common/suspense-with-boundary";
// Server Component
async function ServerData() {
const data = await fetchData();
return <div>{data.content}</div>;
}
// Client Component
function Page() {
return (
<SuspenseWithBoundary
fallback={<Spinner />}
errorFallback={({ error }) => <div>Error: {error.message}</div>}
>
<ServerData />
</SuspenseWithBoundary>
);
}
Custom Loading Component
import { SuspenseWithBoundary } from "@zayne-labs/ui-react/common/suspense-with-boundary";
function LoadingSpinner() {
return (
<div className="loading-container">
<div className="spinner" />
<p>Loading content...</p>
</div>
);
}
function ErrorDisplay({ error, resetErrorBoundary }) {
return (
<div className="error-display">
<h2>Something went wrong</h2>
<details>
<summary>Error details</summary>
<pre>{error.message}</pre>
</details>
<button onClick={resetErrorBoundary}>Retry</button>
</div>
);
}
function App() {
return (
<SuspenseWithBoundary
fallback={<LoadingSpinner />}
errorFallback={ErrorDisplay}
>
<AsyncContent />
</SuspenseWithBoundary>
);
}
Comparison to Native Patterns
Manual Nesting
import { Suspense } from "react";
import { ErrorBoundary } from "@zayne-labs/ui-react/common/suspense-with-boundary";
function App() {
return (
<ErrorBoundary
fallback={({ error, resetErrorBoundary }) => (
<div>
<p>Error: {error.message}</p>
<button onClick={resetErrorBoundary}>Retry</button>
</div>
)}
>
<Suspense fallback={<div>Loading...</div>}>
<AsyncComponent />
</Suspense>
</ErrorBoundary>
);
}
With SuspenseWithBoundary
import { SuspenseWithBoundary } from "@zayne-labs/ui-react/common/suspense-with-boundary";
function App() {
return (
<SuspenseWithBoundary
fallback={<div>Loading...</div>}
errorFallback={({ error, resetErrorBoundary }) => (
<div>
<p>Error: {error.message}</p>
<button onClick={resetErrorBoundary}>Retry</button>
</div>
)}
>
<AsyncComponent />
</SuspenseWithBoundary>
);
}
Common Use Cases
API Route Handler
import { SuspenseWithBoundary } from "@zayne-labs/ui-react/common/suspense-with-boundary";
function ApiDataDisplay({ endpoint }) {
return (
<SuspenseWithBoundary
fallback={
<div className="loading">
<p>Fetching data from {endpoint}...</p>
</div>
}
errorFallback={({ error, resetErrorBoundary }) => (
<div className="error">
<p>Failed to fetch from {endpoint}</p>
<p>{error.message}</p>
<button onClick={resetErrorBoundary}>Retry</button>
</div>
)}
>
<DataComponent endpoint={endpoint} />
</SuspenseWithBoundary>
);
}
Image Gallery
import { SuspenseWithBoundary } from "@zayne-labs/ui-react/common/suspense-with-boundary";
function ImageGallery({ albumId }) {
return (
<SuspenseWithBoundary
fallback={
<div className="gallery-skeleton">
{Array.from({ length: 12 }).map((_, i) => (
<div key={i} className="skeleton-image" />
))}
</div>
}
errorFallback={<div>Failed to load images</div>}
>
<GalleryImages albumId={albumId} />
</SuspenseWithBoundary>
);
}
import { SuspenseWithBoundary } from "@zayne-labs/ui-react/common/suspense-with-boundary";
function SignupForm() {
return (
<form>
<input type="email" name="email" />
<SuspenseWithBoundary
fallback={<div className="validation-spinner" />}
errorFallback={<div className="validation-error">Validation failed</div>}
>
<EmailValidation />
</SuspenseWithBoundary>
<button type="submit">Sign Up</button>
</form>
);
}
The component renders ErrorBoundary on the outside and Suspense on the inside, ensuring errors are caught before suspense boundaries.
Both fallback and errorFallback are optional. If omitted, the component will use the default behavior of Suspense and ErrorBoundary (which may show nothing).
Implementation
The component is a simple composition:
export function SuspenseWithBoundary(props: SuspenseWithBoundaryProps) {
const { children, errorFallback, fallback } = props;
const errorBoundaryProps = Boolean(errorFallback) && { fallback: errorFallback };
const suspenseProps = Boolean(fallback) && { fallback };
return (
<ErrorBoundary {...errorBoundaryProps}>
<Suspense {...suspenseProps}>{children}</Suspense>
</ErrorBoundary>
);
}