React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

swrrscserver-componentshydrationpreloading

SWR with React Server Components

Recipe

Fetch data in a Server Component and pass it to a Client Component as fallback data for SWR. This gives you instant server-rendered content with client-side revalidation — the best of both worlds.

// app/users/page.tsx (Server Component)
import { UserList } from "./user-list";
 
async function getUsers() {
  const res = await fetch("https://api.example.com/users", {
    next: { revalidate: 60 },
  });
  return res.json();
}
 
export default async function UsersPage() {
  const users = await getUsers();
 
  return <UserList fallbackData={users} />;
}
// app/users/user-list.tsx (Client Component)
"use client";
 
import useSWR from "swr";
 
interface User {
  id: string;
  name: string;
}
 
const fetcher = (url: string) => fetch(url).then((r) => r.json());
 
export function UserList({ fallbackData }: { fallbackData: User[] }) {
  const { data: users } = useSWR<User[]>("/api/users", fetcher, {
    fallbackData,
    revalidateOnMount: true, // Re-fetch on mount to get fresh data
  });
 
  return (
    <ul>
      {users?.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

Working Example

// app/dashboard/page.tsx (Server Component)
import { SWRProvider } from "./swr-provider";
import { DashboardStats } from "./dashboard-stats";
import { RecentOrders } from "./recent-orders";
 
async function fetchDashboardData() {
  const [stats, orders] = await Promise.all([
    fetch("https://api.example.com/stats").then((r) => r.json()),
    fetch("https://api.example.com/orders?limit=10").then((r) => r.json()),
  ]);
  return { stats, orders };
}
 
export default async function DashboardPage() {
  const { stats, orders } = await fetchDashboardData();
 
  return (
    <SWRProvider
      fallback={{
        "/api/stats": stats,
        "/api/orders?limit=10": orders,
      }}
    >
      <h1>Dashboard</h1>
      <DashboardStats />
      <RecentOrders />
    </SWRProvider>
  );
}
// app/dashboard/swr-provider.tsx
"use client";
 
import { SWRConfig } from "swr";
 
export function SWRProvider({
  children,
  fallback,
}: {
  children: React.ReactNode;
  fallback: Record<string, any>;
}) {
  return (
    <SWRConfig
      value={{
        fallback,
        fetcher: (url: string) => fetch(url).then((r) => r.json()),
      }}
    >
      {children}
    </SWRConfig>
  );
}
// app/dashboard/dashboard-stats.tsx
"use client";
 
import useSWR from "swr";
 
interface Stats {
  revenue: number;
  orders: number;
  customers: number;
}
 
export function DashboardStats() {
  // Data is instantly available from server fallback
  const { data: stats } = useSWR<Stats>("/api/stats", {
    refreshInterval: 30000, // Keep stats fresh
  });
 
  return (
    <div className="grid grid-cols-3 gap-4">
      <div>Revenue: ${stats?.revenue.toLocaleString()}</div>
      <div>Orders: {stats?.orders}</div>
      <div>Customers: {stats?.customers}</div>
    </div>
  );
}
// app/dashboard/recent-orders.tsx
"use client";
 
import useSWR from "swr";
 
interface Order {
  id: string;
  customer: string;
  total: number;
  status: string;
}
 
export function RecentOrders() {
  const { data: orders } = useSWR<Order[]>("/api/orders?limit=10");
 
  return (
    <table>
      <thead>
        <tr>
          <th>Order</th>
          <th>Customer</th>
          <th>Total</th>
          <th>Status</th>
        </tr>
      </thead>
      <tbody>
        {orders?.map((order) => (
          <tr key={order.id}>
            <td>{order.id}</td>
            <td>{order.customer}</td>
            <td>${order.total}</td>
            <td>{order.status}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}

Deep Dive

How It Works

  • Server Components fetch data at request time (or at build time with static rendering) and pass it down as props or via SWRConfig.fallback.
  • SWRConfig fallback is a map of { [key]: data } that pre-populates the SWR cache on the client.
  • When a useSWR hook mounts with matching fallback data, it renders immediately without a loading state.
  • SWR then revalidates in the background (unless revalidateOnMount: false), ensuring the data stays fresh.
  • This pattern avoids hydration mismatches because the server-rendered HTML matches the initial client render.
  • The preload API can also be used to start fetching before a component mounts.

Variations

Preloading in Server Component:

// app/products/[id]/page.tsx
import { preload } from "swr";
import { ProductDetail } from "./product-detail";
 
const fetcher = (url: string) => fetch(url).then((r) => r.json());
 
export default function ProductPage({ params }: { params: { id: string } }) {
  // Start fetching immediately
  preload(`/api/products/${params.id}`, fetcher);
 
  return <ProductDetail id={params.id} />;
}

Per-page fallback without global provider:

"use client";
 
import useSWR from "swr";
 
export function UserCard({ fallbackData }: { fallbackData: User }) {
  const { data } = useSWR("/api/me", fetcher, {
    fallbackData,               // Per-hook fallback
    revalidateOnMount: false,   // Trust server data, skip initial revalidation
  });
 
  return <div>{data?.name}</div>;
}

Streaming with Suspense:

// Server Component
import { Suspense } from "react";
 
export default function Page() {
  return (
    <div>
      <h1>Dashboard</h1>
      <Suspense fallback={<StatsSkeleton />}>
        <StatsLoader />
      </Suspense>
    </div>
  );
}
 
async function StatsLoader() {
  const stats = await fetch("https://api.example.com/stats").then((r) => r.json());
  return <StatsClient fallbackData={stats} />;
}

TypeScript Notes

  • The fallback prop in SWRConfig is typed as Record<string, any>. There is no compile-time check that keys match your useSWR keys.
  • For stronger typing, create a typed wrapper:
type FallbackMap = {
  "/api/stats": Stats;
  "/api/orders?limit=10": Order[];
};
 
function TypedSWRProvider<T extends Record<string, unknown>>({
  fallback,
  children,
}: {
  fallback: T;
  children: React.ReactNode;
}) {
  return <SWRConfig value={{ fallback }}>{children}</SWRConfig>;
}

Gotchas

  • The fallback keys must exactly match the keys used in useSWR. A mismatch means the fallback data is silently ignored and a loading state appears.
  • Data serialized from Server Components must be JSON-safe. Dates, Maps, Sets, and class instances will not survive the server-to-client boundary.
  • revalidateOnMount: true (the default when fallbackData is provided and no revalidateIfStale is set) causes an immediate re-fetch. Set it to false if the server data is fresh enough.
  • Do not use useSWR directly in Server Components. SWR hooks are client-only. Server Components should use fetch directly or a server-side data layer.
  • When using fallback in SWRConfig, the data is available to all nested useSWR hooks. Be careful about key collisions across unrelated components.

Alternatives

ApproachProsCons
SWRConfig fallbackPre-populated cache, instant renderKeys must match exactly, no type safety on fallback
fallbackData per hookExplicit, component-levelMust thread props from server
Server-only fetch (no SWR)Simplest, no client JSNo client-side revalidation
React Query hydrationBuilt-in dehydrate/hydrateDifferent library, more setup

FAQs

How does the SWRConfig fallback option work with Server Components?

The fallback prop is a { [key]: data } map that pre-populates the SWR cache on the client. When a useSWR hook mounts with a matching key, it renders immediately without a loading state, then revalidates in the background.

What is the difference between fallbackData per-hook and fallback in SWRConfig?
  • fallbackData is set per useSWR hook as a prop: useSWR(key, fetcher, { fallbackData }).
  • fallback in SWRConfig provides data for all nested hooks via a key-value map.
  • Use fallbackData for individual components; use fallback for page-wide pre-population.
Gotcha: What happens if the fallback key does not exactly match the useSWR key?

The fallback data is silently ignored. SWR shows a loading state instead of instant content. Always ensure the keys in your fallback map match the keys used in useSWR calls exactly.

Can I use useSWR directly inside a Server Component?

No. SWR hooks are client-only. Server Components should use fetch directly or a server-side data layer, then pass the data as props or via SWRConfig fallback to Client Components.

Does SWR re-fetch on mount when fallbackData is provided?

By default, yes (revalidateOnMount: true). This ensures the data stays fresh. Set revalidateOnMount: false if the server data is fresh enough and you want to skip the initial client-side fetch.

How do I avoid hydration mismatches when using SWR with Server Components?

Pass server-fetched data through SWRConfig fallback or fallbackData. This ensures the server-rendered HTML matches the initial client render, preventing hydration errors.

Gotcha: What types of data cannot survive the server-to-client boundary?

Data serialized from Server Components must be JSON-safe. Dates, Maps, Sets, and class instances will not survive the boundary. Convert them to plain objects or strings before passing.

How do I use the preload API to start fetching before a component mounts?
import { preload } from "swr";
 
const fetcher = (url: string) => fetch(url).then((r) => r.json());
 
export default function Page({ params }: { params: { id: string } }) {
  preload(`/api/products/${params.id}`, fetcher);
  return <ProductDetail id={params.id} />;
}
How do I type the fallback prop in SWRConfig with TypeScript?

The fallback prop is typed as Record<string, any> -- there is no compile-time check that keys match useSWR keys. For stronger typing, create a typed wrapper:

type FallbackMap = {
  "/api/stats": Stats;
  "/api/orders?limit=10": Order[];
};
 
function TypedSWRProvider<T extends Record<string, unknown>>({
  fallback,
  children,
}: {
  fallback: T;
  children: React.ReactNode;
}) {
  return <SWRConfig value={{ fallback }}>{children}</SWRConfig>;
}
How do I combine Server Component streaming with SWR?

Use <Suspense> around an async Server Component that fetches data and passes it as fallbackData to a Client Component with useSWR. The server streams the HTML as data becomes available.

What happens with key collisions when using fallback across unrelated components?

All nested useSWR hooks sharing the same key receive the same fallback data. Be careful that unrelated components do not accidentally use the same key, which would cause them to share stale or incorrect data.