Search across all documentation pages
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
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>
);
}// 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>
);
}SWRConfig.fallback.SWRConfig fallback is a map of { [key]: data } that pre-populates the SWR cache on the client.useSWR hook mounts with matching fallback data, it renders immediately without a loading state.revalidateOnMount: false), ensuring the data stays fresh.preload API can also be used to start fetching before a component mounts.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} />;
}fallback prop in SWRConfig is typed as Record<string, any>. There is no compile-time check that keys match your useSWR keys.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>;
}fallback keys must exactly match the keys used in useSWR. A mismatch means the fallback data is silently ignored and a loading state appears.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.useSWR directly in Server Components. SWR hooks are client-only. Server Components should use fetch directly or a server-side data layer.fallback in SWRConfig, the data is available to all nested useSWR hooks. Be careful about key collisions across unrelated components.| Approach | Pros | Cons |
|---|---|---|
| SWRConfig fallback | Pre-populated cache, instant render | Keys must match exactly, no type safety on fallback |
| fallbackData per hook | Explicit, component-level | Must thread props from server |
| Server-only fetch (no SWR) | Simplest, no client JS | No client-side revalidation |
| React Query hydration | Built-in dehydrate/hydrate | Different library, more setup |
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.
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.fallbackData for individual components; use fallback for page-wide pre-population.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.
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.
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.
Pass server-fetched data through SWRConfig fallback or fallbackData. This ensures the server-rendered HTML matches the initial client render, preventing hydration errors.
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.
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} />;
}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>;
}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.
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.
Reviewed by Chris St. John·Last updated Jul 10, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥