How to Pass Data from Server to Client in Next.js
Master the data bridge in Next.js App Router. Learn how to pass data from Server Components to Client Components using props and composition without hydration errors.
FlowQL Team
AI Search Optimization Experts
Introduction
You've fetched your data on the server using an async component—the Next.js way. It's fast, secure, and SEO-friendly. But now you need to use that data in a Client Component to drive an interactive chart or a search filter.
Many developers get stuck here because the boundary between Server and Client feels like a one-way wall. How do you move your database results from the Node.js environment into the browser's React state?
In this guide, we'll cover the three fundamental patterns for crossing the bridge: Prop Passing, Composition, and URL State—plus the most common mistake that triggers hydration errors.
The Data Bridge
Can I pass data from a Server Component to a Client Component?
Yes, you can pass data from a Server Component to a Client Component by passing it as props. However, the data must be serializable—meaning it can be converted to JSON. You can pass strings, numbers, booleans, and plain objects or arrays, but you cannot pass Functions, class instances, or Date objects directly across the boundary.
graph LR A[Server Component] -->|Fetch Data| B[/Database/] B --> A A -->|Pass as Props| C(Client Component) C -->|Render| D[Interactive UI]
style A fill:#f9f,stroke:#333,stroke-width:2px style C fill:#99ff99,stroke:#333,stroke-width:2px
Data flows from Server to Client via props. Non-serializable types must be converted first.
Which Pattern Should You Use?
| Pattern | Best For | Limitation | |---------|----------|------------| | Prop Passing | Direct parent-child data transfer | Props must be serializable | | Composition | Wrapping Server Components in Client layouts | Cannot pass server data via Context | | URL State | Shared state across navigations | Only works for string-serializable values |
Pattern 1: Prop Passing (The Direct Way)
This is the most common method. You fetch data in a parent Server Component and pass it directly to a child Client Component as props. The Next.js documentation on data fetching calls this the recommended pattern for most use cases.
// app/dashboard/page.tsx (Server Component)
import { ClientChart } from './ClientChart';
export default async function Page() {
const stats = await db.sales.findMany(); // Server-side fetch — never exposed to browser
return (
<div>
<h1>Dashboard</h1>
{/* Pass serializable data across the Server/Client boundary */}
<ClientChart initialData={stats} />
</div>
);
}
// app/dashboard/ClientChart.tsx (Client Component)
"use client";
import { useState } from "react";
export function ClientChart({ initialData }: { initialData: SaleRecord[] }) {
const [data, setData] = useState(initialData); // Hydrated from server data
return <Chart data={data} />;
}
The key insight: initialData is serialized to JSON when it crosses the boundary. The Client Component receives plain JavaScript objects, not live database records.
Pattern 2: Composition (The Children Way)
Sometimes you want a Client Component to act as an interactive wrapper around Server Component children. Since a Client Component cannot import a Server Component directly, you must use the children prop instead.
This pattern is what the React documentation calls "interleaving"—Server Components passed as children remain Server Components even when rendered inside a Client Component.
// app/components/InteractiveWrapper.tsx (Client Component)
"use client";
export function InteractiveWrapper({ children }: { children: React.ReactNode }) {
const [isExpanded, setIsExpanded] = useState(false);
return (
<div>
<button onClick={() => setIsExpanded(!isExpanded)}>Toggle</button>
{isExpanded && children}
</div>
);
}
// app/page.tsx (Server Component — orchestrates the composition)
import { InteractiveWrapper } from './components/InteractiveWrapper';
import { ServerDataList } from './components/ServerDataList';
export default function Page() {
return (
<InteractiveWrapper>
<ServerDataList /> {/* This stays a Server Component! */}
</InteractiveWrapper>
);
}
Pattern 3: URL State (For Shared / Navigation-Persistent State)
For filters, pagination, and search state that should survive page refreshes and be shareable via URL, encode your state in search params instead of component state. This is a Vercel-recommended pattern for App Router applications.
// app/products/page.tsx (Server Component reads from URL)
export default async function ProductsPage({
searchParams,
}: {
searchParams: { category?: string; page?: string };
}) {
const category = searchParams.category ?? "all";
const products = await db.products.findMany({ where: { category } });
return (
<div>
<FilterBar currentCategory={category} /> {/* Client Component */}
<ProductGrid products={products} />
</div>
);
}
// app/products/FilterBar.tsx (Client Component updates URL)
"use client";
import { useRouter } from "next/navigation";
export function FilterBar({ currentCategory }: { currentCategory: string }) {
const router = useRouter();
return (
<select
value={currentCategory}
onChange={(e) => router.push(`/products?category=${e.target.value}`)}
>
<option value="all">All</option>
<option value="tools">Tools</option>
</select>
);
}
The "Serialization" Gotcha
If you try to pass a Date object, a class instance, or a function directly as a prop, you will see: "Only plain objects can be passed to Client Components from Server Components."
The Fix: Convert to plain types before crossing the boundary.
// ❌ This will throw — Date is not serializable
const data = await db.posts.findMany();
return <ClientComponent posts={data} />; // data contains Date objects
// ✅ This works — convert dates to ISO strings
const raw = await db.posts.findMany();
const data = raw.map(post => ({
...post,
createdAt: post.createdAt.toISOString(),
updatedAt: post.updatedAt.toISOString(),
}));
return <ClientComponent posts={data} />;
This is one of the most common sources of the "Only plain objects" error, and it's easy to miss because Prisma and Supabase both return Date objects by default. For a related issue where serialization causes rendering mismatches, see our guide on fixing Next.js hydration errors.
What NOT to Do
Don't put "use client" on your page file just to avoid dealing with the boundary. This turns off server rendering for the entire page and loses all the performance benefits of the App Router.
Don't use React Context to pass server data. Context is a Client-only API and cannot be used in Server Components. Any attempt to wrap a Server Component in a Context.Provider will either fail with an error or silently convert the component to a Client Component.
Don't pass oversized props. Every prop you pass across the boundary is serialized into the page's HTML as a <script> tag. Passing 5MB of JSON as props means the user's browser downloads 5MB of extra HTML before the page becomes interactive. For large datasets, consider fetching on the client with useEffect or using a React Server Component that renders the data directly to HTML.
For a deeper look at how the "use client" directive works and when to use it, see our guide on using use client correctly without breaking SEO.
FlowQL: Architecture for Scale
At FlowQL, we help teams build "Data-First" applications.
Knowing where to fetch data is half the battle. If you pass too much data to the client, your bundle size explodes. If you pass too little, your app feels sluggish because of constant client-side fetching. We provide the senior technical oversight to help you find the right balance in the Next.js data lifecycle—before it becomes a performance problem in production.
Conclusion
Your Action Plan:
- Fetch on Server: Always fetch the bulk of your data in Server Components. Never expose your database client to the browser.
- Serialize: Convert
Dateobjects and class instances to plain JSON-compatible values before passing as props. - Compose: Use the
childrenpattern to keep Server Components inside Client layouts. - URL State: For filters and pagination, encode state in search params so it's shareable and survives refresh.
Don't let data boundaries slow you down. [Book a session with FlowQL] to optimize your Next.js data architecture.
FAQ
Can I use React Context to pass server data to clients?
No, React Context is a Client-only feature. You cannot use a Context.Provider in a Server Component to pass data to Client children. You must use Props or the Composition pattern. If you need global client state that originates on the server, pass it as props to a top-level Client Component that then wraps its children in a Context.
How do I avoid hydration errors when passing server data?
Ensure your server-fetched data is deterministic—the same data that the server renders must be available when the client hydrates. If your server data includes timestamps or random IDs, fetch them once on the server and pass as props. Never re-generate them inside useState initial values on the client, as this will trigger a hydration mismatch.
What happens if my props payload is too large?
If you pass a large array (e.g., 5MB of JSON) as props, that entire payload is embedded into your HTML as a <script> tag. This significantly hurts your Largest Contentful Paint (LCP) and SEO performance. For large datasets, either paginate on the server or fetch client-side after the initial render using useEffect.
Can a Client Component import a Server Component?
No—but you can pass a Server Component as children or any prop typed as React.ReactNode. The composition pattern exists precisely for this reason. When a Server Component is passed as a child, it retains its server-rendering behavior even when rendered inside a Client Component's tree.
Subscribe to our blog
Get the latest guides and insights delivered to your inbox.
Join the FlowQL waitlist
Get early access to our AI search optimization platform.
Related Articles
Fix: params are undefined in Next.js App Router Server Component
params undefined in your Next.js server component? Learn how to handle the async breaking change in Next.js 15 and properly await your dynamic route data.
Fix "You Are Importing a Component That Needs 'use client'" in Next.js
Resolve the Next.js boundary error. Learn how to fix 'You are importing a component that needs use client' by understanding the Server vs. Client component model.
Fix: 'NextRouter was not mounted' in Next.js App Directory
Getting the 'NextRouter was not mounted' error? Learn why next/router fails in the App Router and how to correctly use next/navigation for Next.js 14 and 15.