Skip to Content
FrontendSSR & Hydration

SSR & Hydration

The dominant data pattern across all apps. Server Components fetch data and pass it as initialData to client wrappers. SWR then takes over for subsequent interactions.

For generated API clients and SWR hooks see Data Layer. For Context architecture see State Management.

The initialData Hydration Pattern

Step 1: Server Component Fetches

// app/[locale]/(pages)/catalog/page.tsx — Server Component import { createCatalogPublicClient } from '@lib/api/server'; import { getCatalogPublicVehiclesList } from '@api/generated/catalog_public/_utils/fetchers'; export default async function CatalogPage({ searchParams }) { const client = createCatalogPublicClient(); const [vehicles, brands] = await Promise.all([ getCatalogPublicVehiclesList({ page: 1, page_size: 20 }, client).catch(() => null), getCatalogPublicBrandsList(undefined, client).catch(() => []), ]); return ( <CatalogClientWrapper initialVehicles={vehicles} initialBrands={brands} /> ); }

Use Promise.all for parallel fetches and .catch(() => null) for graceful fallback when individual fetches fail.

Step 2: Client Component Hydrates

// CatalogClientWrapper.tsx — 'use client' 'use client'; export function CatalogClientWrapper({ initialVehicles, initialBrands }) { return ( <CatalogProvider> <FiltersProvider> <VehiclesProvider initialData={initialVehicles}> <CatalogLayout /> </VehiclesProvider> </FiltersProvider> </CatalogProvider> ); }

Step 3: SWR Takes Over

// VehiclesContext.tsx const { data: swrData, isLoading } = useCatalogPublicVehiclesList(apiParams, client); // Prefer SWR data over SSR initialData const vehicles = swrData ?? initialData;

SWR revalidates in the background. On first render the SSR data is shown immediately — no loading spinner.

force-dynamic for Auth-Dependent Pages

Pages that depend on authenticated state or real-time data disable static generation:

export const dynamic = 'force-dynamic';

Used in: dashboards, project editors, all private routes.

Client-Only Rendering

For highly interactive pages (video editors, complex canvas), SSR is skipped entirely with a mounted-state gate:

// ProjectEditorView.tsx const [mounted, setMounted] = useState(false); useEffect(() => { setMounted(true); }, []); if (!mounted) return <EditorLoader />; return <EditorLayout />;

Alternatively, use Next.js dynamic import with ssr: false:

const EditorView = dynamic(() => import('./EditorView'), { ssr: false });

Static Generation for Locale Segments

Only locale parameters are statically generated — content pages use SSR:

export function generateStaticParams() { return LOCALES.map((locale) => ({ locale })); }

Metadata Generation

Server Components handle Next.js metadata via generateMetadata:

export async function generateMetadata({ params }: Props): Promise<Metadata> { const t = await getTranslations('catalog'); return generateAppMetadata({ title: t('meta.title'), description: t('meta.description'), }); }

The generateAppMetadata helper from @core/metadata adds OG images, canonical URLs, and other defaults.

Data Shape Adaptation

When SSR-fetched data has a different shape than what components expect, adapt with useMemo:

// SavedView.tsx — adapt VehicleDetail to VehicleList shape const listItem = useMemo(() => ({ ...data, main_photo_url: data.photos[0]?.url ?? null, photos_count: data.photos.length, source: { id: data.source.id, code: data.source.code, name: data.source.name }, }), [data]);

Rules

  1. Server Components fetch data and pass it as initialData props — never fetch data inside client components directly
  2. Use Promise.all with .catch(() => null) for parallel SSR fetches with graceful fallback
  3. SWR takes over after hydration — prefer swrData ?? initialData to show SSR data immediately
  4. Set export const dynamic = 'force-dynamic' on auth-dependent and real-time pages
  5. Use dynamic(() => import(...), { ssr: false }) or a mounted-state gate for client-only editors
  6. Data shape mismatches between server and client are resolved with useMemo in the view component
Last updated on