Pass props across the server and client boundary the same way you pass any React prop, as long as the value is serializable. Fetch or build the data in the Server Component, hand it to the Client Component as a prop, and let that component own the interactive parts, such as state and event handlers.
// app/dashboard/page.tsx
import TagFilter from '@/app/ui/tag-filter'
async function getTags() {
const res = await fetch('https://api.example.com/tags')
return res.json() as Promise<string[]>
}
export default async function DashboardPage() {
const tags = await getTags()
return <TagFilter tags={tags} />
}This page is a Server Component, so it runs only on the server. It fetches an array of tag strings and passes that array straight into TagFilter as a prop named tags.
// app/ui/tag-filter.tsx
'use client'
import { useState } from 'react'
export default function TagFilter({ tags }: { tags: string[] }) {
const [selected, setSelected] = useState<string | null>(null)
return (
<ul>
{tags.map((tag) => (
<li key={tag}><button onClick={() => setSelected(tag)}>{tag}</button></li>
))}
</ul>
)
}TagFilter runs on the client. The use client directive is there because this component needs useState and a click handler, neither of which a Server Component can use. Clicking a tag button updates the highlighted tag locally with no server round trip.
Why the prop has to be serializable
Next.js sends the props for a Client Component inside the React Server Component payload, a serialized data format the browser can parse. Anything that cannot be represented in that format never reaches the client, so React blocks it at build or render time instead of sending broken data.
| Passes as a prop | Does not pass as a prop |
|---|---|
| string, number, boolean, null | function or event handler defined on the server |
| plain object or array | class instance, other than Date, Map, or Set |
| Date, Map, Set | Symbol that is not registered with Symbol.for |
| Promise, for streaming with use() | React elements returned from a function you call yourself |
The tags array in the example above is a plain array of strings, so it serializes without any extra work. If you tried to pass a function, such as an onTagClick handler defined in the Server Component, the build would fail instead. That exact failure is covered in Fixing "Functions cannot be passed directly to Client Components".
For a longer list of what counts as serializable and why, see What Props Can You Pass from Server to Client Components in Next.js. For a wider explanation of the directive itself, see The use client Directive Explained.
Streaming a Promise as a prop
A Promise is one of the few non-plain values React can serialize, and passing one lets a Client Component start rendering before the data behind it finishes loading. This is the practical variation on the pattern above, useful when a fetch is slow enough that you do not want it to block the whole page.
// app/blog/page.tsx
import Posts from '@/app/ui/posts'
import { Suspense } from 'react'
export default function Page() {
const posts = getPosts()
return (
<Suspense fallback={<p>Loading posts...</p>}>
<Posts posts={posts} />
</Suspense>
)
}Page still runs on the server, but it deliberately does not await getPosts(). Skipping the await is what makes this a Promise prop instead of a resolved value, and the Suspense boundary tells Next.js what to show while that promise is still pending.
// app/ui/posts.tsx
'use client'
import { use } from 'react'
type Post = { id: string; title: string }
export default function Posts({ posts }: { posts: Promise<Post[]> }) {
const allPosts = use(posts)
return (
<ul>
{allPosts.map((post) => <li key={post.id}>{post.title}</li>)}
</ul>
)
}Posts runs on the client and needs use client because use() only works inside a Client Component. Calling it with the posts promise suspends this component until the promise resolves, so the browser first shows the Suspense fallback, then swaps in the rendered list once the data arrives.
Common mistakes
- Awaiting the data in the Server Component before passing it down, which removes the streaming benefit and turns the Promise back into a resolved value.
- Passing a Promise prop into a component that is not wrapped in a Suspense boundary, which throws instead of showing a fallback.
- Passing a function, such as an inline click handler, from a Server Component into a Client Component, which fails because functions are not serializable.
- Passing a class instance, such as a custom model object, instead of converting it to a plain object first.
- Forgetting
use clienton the component that needs state, an event handler, or the use hook.
Rune AI
Key Insights
- Props sent from a Server Component to a Client Component must be serializable.
- Strings, numbers, booleans, null, plain objects, arrays, Date, Map, and Set all serialize fine.
- Functions, event handlers, and class instances cannot cross the boundary as plain props.
- A Promise can be passed as a prop and read on the client with the use API for streaming.
- Wrap a component that reads a Promise prop with use() in a Suspense boundary.
- Do not await a Promise on the server before passing it down if you want it to stream.
Frequently Asked Questions
Can I pass a Date, Map, or Set as a prop to a Client Component?
Why does my prop show up as an empty object in the Client Component?
Do I need use client on the component that receives the props?
Can I pass a Server Component itself as a prop instead of data?
Conclusion
Passing props across the server and client boundary works the same way passing any prop does, as long as the value is serializable. Fetch or compute the data in the Server Component, pass it down as a prop, and let the Client Component own state and event handlers with that data.Passing props across the server and client boundary is ordinary prop passing with one extra rule: the value has to be serializable. Plain objects, arrays, strings, numbers, booleans, null, Date, Map, and Set all work directly, and a Promise works too if the receiving Client Component reads it with use() inside a Suspense boundary. Keep the interactive parts, like state and handlers, defined in the Client Component itself instead of trying to send them from the server.
Reach for the plain prop pattern first, since it is simpler and covers most cases where the data is already available when the page renders. Reach for the Promise and use() pattern only when a specific fetch is slow enough that blocking the whole page on it would hurt the loading experience.
More in this topic
`generateMetadata` Explained with Real Examples
What generateMetadata does, when it runs, and how to use it for real routes: awaited params, deduplicated data fetching, extending parent metadata, and returning a 404 from metadata.
Canonical URLs in Next.js: `metadataBase`, `alternates.canonical`, and Dynamic Pages
How canonical URLs work in the Next.js App Router: setting metadataBase once, writing alternates.canonical per route, handling dynamic segments, and what happens when the base URL is missing.
Open Graph and Twitter Card Metadata in Next.js
How to write Open Graph and Twitter card metadata in the Next.js App Router: the openGraph and twitter fields, automatic card defaults, article tags, and image merge rules.