Skip to content

Instantly share code, notes, and snippets.

@djstein
Last active July 23, 2025 19:00
Show Gist options
  • Save djstein/5681d0321fe2b0c997f68f50fed3c56d to your computer and use it in GitHub Desktop.
Save djstein/5681d0321fe2b0c997f68f50fed3c56d to your computer and use it in GitHub Desktop.
component with suspense and error boundary
import {
PokemonCard,
PokemonCardComponent,
} from "@ui/components/user/pokemon-card";
export default function Page() {
return (
<div className="flex items-center justify-center gap-2">
<PokemonCard
name="Venusaur"
hp={120}
description="Venusaur is a Grass/Poison type Pokémon known for the large flower on its back, which is just about to bloom."
number={3}
/>
<PokemonCard
name="Charizard"
hp={120}
description="Charizard is a Fire/Flying type Pokémon known for its fierce temperament and powerful fire-based attacks."
number={6}
/>
<PokemonCard
name="Blasteoise"
hp={120}
description="Blastoise is a Water type Pokémon known for its powerful water-based attacks and its ability to Mega Evolve."
number={9}
/>
<PokemonCardComponent
name="Bulbasaur"
hp={60}
description="Bulbasaur is a Grass/Poison type Pokémon known for its plant bulb that grows into a large flower as it evolves."
number={1}
/>
</div>
);
}
"use client";
import * as Sentry from "@sentry/nextjs";
import { Suspense, useEffect } from "react";
import { ErrorBoundary } from "react-error-boundary";
import { Skeleton } from "../ui/skeleton";
//// I really wanted to do Dot Notation ie. PokemonCard.Component
/// but Next.js breaks! the react docs used to encourage this approach
/// but now they say to explicitly export everything
/// https://github.com/vercel/next.js/issues/51593
///
export type PokemonCardProps = {
name: string;
hp: number;
description: string;
number: number;
};
export function PokemonCardComponent({
name,
hp,
description,
number,
}: PokemonCardProps) {
// 1. use this to simulate a client-only component
// uncomment the following lines to test client-only rendering which triggers suspense
// if (typeof window === "undefined") {
// throw Error("Pokemon cards should only render on the client.");
// }
// 2. use this to simulate suspense
// throw new Promise(() => {});
// 3. use this to simulate an error
// useEffect(() => {
// throw new Error("This is a simulated error for testing purposes.");
// }, []);
return (
<div className="p-2 w-64 border rounded-lg flex flex-col gap-2 h-96">
<div className="flex justify-between">
<h2>{name}</h2>
<h2>{hp} HP</h2>
</div>
<div className="border rounded-lg h-64">
<img
src={`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${number}.png`}
alt={name}
className="w-full h-auto"
/>
</div>
<div className="border rounded-lg">
<p className="text-sm">{description}</p>
</div>
</div>
);
}
export function PokemonCardSkeleton(): React.ReactNode {
return (
<div className="p-2 w-64 border rounded-lg flex flex-col gap-2 h-96">
<div>
<Skeleton className="h-6" />
</div>
<div className="border rounded-lg">
<Skeleton className="h-48" />
</div>
<div className="border rounded-lg">
<Skeleton className="h-32" />
</div>
</div>
);
}
export function PokemonCardErrorFallback({
error,
resetErrorBoundary,
}: {
error: Error;
resetErrorBoundary: () => void;
}): React.ReactNode {
useEffect(() => {
// Log the error to Sentry
// this also logs it to the console via
// react-client-callbacks/error-boundary-callbacks
Sentry.captureException(error);
}, [error]);
return (
<div className="p-2 w-64 border rounded-lg flex flex-col gap-2 h-96">
<p>Error</p>
<pre className="text-wrap">{error.message}</pre>
<button
className="border rounded-md px-2 py-1"
onClick={resetErrorBoundary}
>
Try again
</button>
</div>
);
}
// option 1: have the card, skeleton, and error fallback simply have similar styles on them to match heights
export function PokemonCard(props: PokemonCardProps): React.ReactNode {
return (
<ErrorBoundary FallbackComponent={PokemonCardErrorFallback}>
<Suspense fallback={<PokemonCardSkeleton />}>
<PokemonCardComponent {...props} />
</Suspense>
</ErrorBoundary>
);
}
// option 2: have a wrapper component that applies the top level styles
export function PokemonCardV2(props: PokemonCardProps): React.ReactNode {
return (
<div className="p-2 w-64 border rounded-lg flex flex-col gap-2 h-96">
<ErrorBoundary FallbackComponent={PokemonCardErrorFallback}>
<Suspense fallback={<PokemonCardSkeleton />}>
<PokemonCardComponent {...props} />
</Suspense>
</ErrorBoundary>
</div>
);
}
// from shadcn
import { cn } from "@ui/lib/utils";
function Skeleton({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse rounded-md bg-primary/10", className)}
{...props}
/>
);
}
export { Skeleton };
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment