Next.js & SSR
Server-rendered pitches under the App Router, and the one boundary rule to know.
@pitchkit/react server-renders to real SVG — verified against renderToString in the
test suite and against a real Next.js App Router app (examples/react-nextjs/ in the
repo). The initial HTML response contains the full pitch and its marks; hydration attaches
interactivity without re-drawing anything.
The one rule: originate the tree in a client component
Every layer takes accessor functions as props (x={(p) => p.x}), and React Server
Components cannot pass functions as props across the server → client boundary. Compose
your <Pitch> tree inside a "use client" component, and render that from your page:
// app/ShotMapPanel.tsx
"use client";
import { Pitch, Scatter } from "@pitchkit/react";
import { shots } from "./data";
export function ShotMapPanel() {
return (
<Pitch type="statsbomb">
<Scatter data={shots} x={(s) => s.x} y={(s) => s.y} />
</Pitch>
);
}// app/page.tsx — stays a Server Component
import { ShotMapPanel } from "./ShotMapPanel";
export default function Page() {
return <ShotMapPanel />;
}Putting the <Pitch> tree directly in a Server Component page fails next build with
"Functions cannot be passed directly to Client Components" — that's this rule, not a
PitchKit limitation.
"use client" governs hydration and prop serialization, not whether SSR happens — the
panel above is still fully server-rendered into the initial HTML.
Density layers are client-only by design
<Heatmap>, <PositionalHeatmap>, <Hexbin> and <KDE> paint to a <canvas>, which has no
server-renderable content — they render nothing on the server and paint after hydration. The
pitch and any SVG layers around them still SSR normally.
<PlayerAvatar> is client-only too. It is HTML in a layer stacked
over the pitch, so its photo can be next/image, and it appears after hydration rather than in the
server HTML. Give next/image the photo host in images.remotePatterns, as for any remote image.
Data fetching
Fetch on the server as usual and pass plain data down — arrays and objects serialize fine across the boundary; only functions don't. Accessors live in the client component, next to the JSX that uses them.