React 19 patterns with Bun runtime, SSR, hydration, and modern React features for Pokemon TCG platform.
Define patterns for React 19 server-side rendering with Bun runtime, focusing on SSR hydration, streaming, and Pokemon TCG UI development.
High
ALWAYS implement SSR for initial page loads (ID: SSR_FIRST)
// Server-side rendering entry
import React from 'react';
import { renderToReadableStream, renderToString } from 'react-dom/server';
import { getBrowserJavascriptBundle } from './fs';
import App from '../../../../web/App';
import ServerErrorPage from '../../../../web/pages/ServerErrorPage';
export async function renderWebApp() {
const bundle = await getBrowserJavascriptBundle();
if (!bundle) {
return new Response(
renderToString(
<ServerErrorPage error={new Error('Missing Web Assets')} />
),
{
status: 500,
statusText: 'Internal Server Error',
headers: { 'Content-Type': 'text/html; charset=utf-8' }
}
);
}
const stream = await renderToReadableStream(<App />, {
bootstrapScriptContent: `window.__INITIAL_STATE__ = ${JSON.stringify({})}`,
bootstrapModules: [bundle]
});
return new Response(stream, {
headers: { 'Content-Type': 'text/html; charset=utf-8' }
});
}
ALWAYS hydrate on the client (ID: CLIENT_HYDRATE)
// Client-side hydration
import React from 'react';
import { hydrateRoot } from 'react-dom/client';
import App from '../App';
hydrateRoot(document, <App />);
ALWAYS use canary features when beneficial (ID: USE_CANARY)
// tsconfig.json
{
"compilerOptions": {
"types": ["react/canary", "react-dom/canary"]
}
}
ALWAYS use React.use() for async data (ID: USE_HOOK)
import { use } from 'react';
function PokemonCard({ cardPromise }: { cardPromise: Promise<Card> }) {
const card = use(cardPromise);
return <div>{card.name}</div>;
}
ALWAYS use Bun.serve() for SSR server (ID: BUN_SERVE)
import { serve } from './server/server';
const server = serve();
console.log(`Server running on http://localhost:${server.port}`);
ALWAYS use Bun native APIs (ID: BUN_NATIVE)
// Use Bun.file for file operations
const data = await Bun.file('./data/cards.json').json();
// Use Bun.write for writing files
await Bun.write('./dist/index.html', html);
NEVER use Node.js-specific APIs incompatible with Bun (ID: NO_NODE_SPECIFIC)
ALWAYS use functional components (ID: FUNCTIONAL_ONLY)
ALWAYS export with React.memo for optimization (ID: MEMO_EXPORT)
import React from 'react';
function PokemonCard({ card }: PokemonCardProps) {
return (
<div className="pokemon-card">
<h3>{card.name}</h3>
<p>HP: {card.hp}</p>
</div>
);
}
export default React.memo(PokemonCard);
ALWAYS follow this structure (ID: COMPONENT_STRUCTURE)
ComponentName/
├── index.ts # Barrel export
├── Component.tsx # Logic and Data Layer
├── View.tsx # View/Markup Layer
├── ComponentName.css # Component styles
├── types.ts # TypeScript types
└── __tests__/
└── ComponentName.test.tsx
ALWAYS fetch data on server when possible (ID: SERVER_FETCH)
// Server-side data fetching
export async function getServerSideProps() {
const cards = await fetch('http://localhost:8080/graphql', {
method: 'POST',
body: JSON.stringify({ query: '{ cards { id name } }' })
}).then((r) => r.json());
return { props: { cards } };
}
ALWAYS handle loading states with Suspense (ID: USE_SUSPENSE)
import { Suspense } from 'react';
function CardGrid() {
return (
<Suspense fallback={<LoadingSpinner />}>
<PokemonCardList />
</Suspense>
);
}
ALWAYS define component props interfaces (ID: PROPS_INTERFACE)
import { PokemonCard as CardData } from '@pokemon/pokemon-data';
export interface PokemonCardProps {
card: CardData;
onSelect?: (card: CardData) => void;
variant?: 'grid' | 'list' | 'detail';
className?: string;
}
ALWAYS type children prop explicitly (ID: TYPE_CHILDREN)
interface LayoutProps {
children: React.ReactNode;
title?: string;
}
ALWAYS use React.memo for expensive components (ID: MEMO_EXPENSIVE)
ALWAYS use useMemo for expensive computations (ID: USE_MEMO)
import { useMemo } from 'react';
function CardGrid({ cards }: { cards: Card[] }) {
const sortedCards = useMemo(() => {
return [...cards].sort((a, b) => a.name.localeCompare(b.name));
}, [cards]);
return <div>{sortedCards.map(card => <Card key={card.id} card={card} />)}</div>;
}
ALWAYS use useCallback for event handlers passed to children (ID: USE_CALLBACK)
import { useCallback } from 'react';
function CardSelector({ onSelect }: { onSelect: (id: string) => void }) {
const handleClick = useCallback((id: string) => {
onSelect(id);
}, [onSelect]);
return <Card onClick={handleClick} />;
}
ALWAYS ensure server and client render the same (ID: MATCH_RENDERS)
NEVER use browser-only APIs during render (ID: NO_BROWSER_APIS)
// Bad
function Component() {
const width = window.innerWidth; // Error on server!
return <div>{width}</div>;
}
// Good
function Component() {
const [width, setWidth] = useState(0);
useEffect(() => {
setWidth(window.innerWidth);
}, []);
return <div>{width || 'Loading...'}</div>;
}
ALWAYS import CSS in component files (ID: IMPORT_CSS)
import './PokemonCard.css';
function PokemonCard() {
return <div className="pokemon-card">...</div>;
}
ALWAYS use CSS modules or scoped selectors (ID: SCOPED_STYLES)
ALWAYS wrap components with error boundaries (ID: ERROR_BOUNDARIES)
import { ErrorBoundary } from 'react-error-boundary';
function App() {
return (
<ErrorBoundary fallback={<ErrorFallback />}>
<CardBrowser />
</ErrorBoundary>
);
}
ALWAYS test components with Bun test runner (ID: BUN_TEST)
import { expect, test } from 'bun:test';
import { render } from '@testing-library/react';
import PokemonCard from './PokemonCard';
test('renders card name', () => {
const card = { id: '1', name: 'Pikachu', hp: '60' };
const { getByText } = render(<PokemonCard card={card} />);
expect(getByText('Pikachu')).toBeDefined();
});
ALWAYS use bun for bundling for production (ID: BUN_BUILDER)
// src/build/browser.ts
await Bun.build({
entrypoints: ['src/web/browser/index.tsx'],
jsx: {
runtime: 'automatic',
importSource: 'react'
},
outdir: './out',
naming: {
entry: 'www/browser.[hash].js'
},
target: 'browser',
format: 'esm',
packages: 'bundle',
splitting: false,
sourcemap: 'linked',
minify: false,
root: '.'
});
ALWAYS handle card image loading gracefully (ID: LAZY_IMAGES)
function PokemonCardImage({ src, alt }: { src: string; alt: string }) {
return (
<img
src={src}
alt={alt}
loading="lazy"
onError={(e) => {
e.currentTarget.src = '/placeholder-card.png';
}}
/>
);
}
ALWAYS virtualize large card lists (ID: VIRTUALIZE_LISTS)
import { useVirtualizer } from '@tanstack/react-virtual';
function CardGrid({ cards }: { cards: Card[] }) {
const virtualizer = useVirtualizer({
count: cards.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 300
});
// ... render virtual items
}
// CardBrowser.tsx
import React, { Suspense } from 'react';
import './CardBrowser.css';
interface CardBrowserProps {
initialCards?: Card[];
}
function CardBrowser({ initialCards = [] }: CardBrowserProps) {
return (
<div className="card-browser">
<h1>Pokemon Cards</h1>
<Suspense fallback={<LoadingGrid />}>
<CardGrid cards={initialCards} />
</Suspense>
</div>
);
}
export default React.memo(CardBrowser);
// apps/web/src/server/server.ts
import { handleRequest } from './lib/handleRequest';
import { middleware } from './lib/middleware/middleware';
export const serve = () => {
return Bun.serve({
port: 3000,
async fetch(req) {
middleware(req);
return handleRequest(req);
}
});
};