Last active
July 23, 2025 19:00
-
-
Save djstein/5681d0321fe2b0c997f68f50fed3c56d to your computer and use it in GitHub Desktop.
component with suspense and error boundary
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | |
); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"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> | |
); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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