Next.js AppRouter divides your app into server components and client components. Server components run on the server and store secrets. Client components run in the browser and handle the interaction. The challenge is to share data and UI between them without breaking the rules of each environment.
This guide shows you how to share components and data between server and client components in Next.js. You’ll learn composition patterns, prop passing rules, and when to use each approach.
Table of Contents
What are the server and client components?
In Next.js AppRouter, every component is a server component by default. Server components run only on the server. They can retrieve data from databases, use API keys, and keep sensitive logic outside of the browser. They do not send JavaScript to the client, which reduces the bundle size.
Client components run on both the server (for basic HTML) and the client (for interactivity). You mark them with "use client" directive at the top of the file. They can use useState, useEffectevent handlers, and browser APIs such as localStorage And window.
Key Principles: Server components can import and render client components, but client components cannot directly import server components. They can only receive them as support (eg children).
Conditions
Before you proceed, you must:
Basic familiarity with React (components, props, hooks)
A Next.js project using AppRouter (Next.js 13 or later)
Node.js installed (version 18 or later recommended)
If you don’t have a Next.js project yet, create one with:
npx create-next-app@latest my-app
How to transfer data from server to client using props
The simplest way to share data between server and client components is to pass it as props. The server component receives the data, and the client component receives it and handles the interaction.
Here is a basic example. A page (server component) fetches a post and gets the number of likes to a. LikeButton (Client Components):
// app/post/(id)/page.jsx (Server Component)
import LikeButton from '@/app/ui/like-button';
import { getPost } from '@/lib/data';
export default async function PostPage({ params }) {
const { id } = await params;
const post = await getPost(id);
return (
);
}
// app/ui/like-button.jsx (Client Component)
'use client';
import { useState } from 'react';
export default function LikeButton({ likes, postId }) {
const (count, setCount) = useState(likes);
const handleLike = () => {
setCount((c) => c + 1);
// Call API or Server Action to persist
};
return (
);
}
The server component fetches data on the server. The client component receives simple values ​​(likes, postId) and manages state and events. This pattern maintains data retrieval on the server and interactions on the client.
How to pass server components as children to client components
You can pass server components as children Support (or no support) for client components. The server component still renders on the server. The client component receives the rendered output, not the component code.
This is useful when you want the client component to wrap or control the ordering of content served by the server. For example, a modal that displays data received from a server:
// app/ui/modal.jsx (Client Component)
'use client';
import { useState } from 'react';
export default function Modal({ children }) {
const (isOpen, setIsOpen) = useState(false);
return (
<>
{isOpen && (
)}
>
);
}
// app/cart/page.jsx (Server Component)
import Modal from '@/app/ui/modal';
import Cart from '@/app/ui/cart';
export default function CartPage() {
return (
);
}
// app/ui/cart.jsx (Server Component - no 'use client')
import { getCart } from '@/lib/cart';
export default async function Cart() {
const items = await getCart();
return (
{items.map((item) => (
- {item.name}
))}
);
}
Cart There is a server component that receives cart data. has passed as children To Modalwhich is a client component. The server offers Cart The first RSC payload contains the rendered result. The client receives that output and displays it inside the modal. Cart data is never run on the client.
You can use the same pattern with named props (slots):
// app/ui/tabs.jsx (Client Component)
'use client';
import { useState } from 'react';
export default function Tabs({ tabs, children }) {
const (activeIndex, setActiveIndex) = useState(0);
return (
{tabs.map((tab, i) => (
))}
{children(activeIndex)}
);
}
// app/dashboard/page.jsx (Server Component)
import Tabs from '@/app/ui/tabs';
import Overview from '@/app/ui/overview';
import Analytics from '@/app/ui/analytics';
export default function DashboardPage() {
const tabs = (
{ id: 'overview', label: 'Overview' },
{ id: 'analytics', label: 'Analytics' },
);
return (
);
}
Overview And Analytics A server may have components that fetch their own data. They render to the server, and the client receives the pre-rendered output.
What props are allowed between the server and client.
There must be props passed from the server component to the client component Streamable. React serializes them into an RSC payload to be sent to the client.
Allowed types
Strings, Numbers, Boolean
nullAndundefinedSimple objects (no functions, no class instances)
Arrays of serializable values.
JSX (server components as children or other supports)
Works with server actions (
"use server")
Not allowed.
Actions (except server actions)
DateItemsClass instances.
Symbols
Map,Set,WeakMap,WeakSetItems with custom prototypes
buffers,
ArrayBufferTyped arrays
If you need to pass. Dateconvert it to a string or number first:
Change if you use MongoDB. ObjectId to a string:
Passing server actions as props
ServerActions are async functions marked with "use server". You can pass them as props to the client component. They are ordered by reference, not by value.
// app/actions/post.js
'use server';
export async function likePost(postId) {
// Update database
revalidatePath(`/post/${postId}`);
}
// app/post/(id)/page.jsx (Server Component)
import LikeButton from '@/app/ui/like-button';
import { likePost } from '@/app/actions/post';
export default async function PostPage({ params }) {
const post = await getPost((await params).id);
return ;
}
// app/ui/like-button.jsx (Client Component)
'use client';
export default function LikeButton({ likes, postId, onLike }) {
const handleClick = () => {
onLike(postId);
};
return ;
}
You can also bind arguments:
How to share context and data with React.cache
React context does not work in server components. To share data between server and client components, you can associate the client component’s context provider with it. React.cache For server-side memoization.
Create a cached fetch function:
// lib/user.js
import { cache } from 'react';
export const getUser = cache(async () => {
const res = await fetch('
return res.json();
});
Create a provider that accepts a promise and stores it in a context:
// app/providers/user-provider.jsx
'use client';
import { createContext } from 'react';
export const UserContext = createContext(null);
export default function UserProvider({ children, userPromise }) {
return (
{children}
);
}
In your route layout, pass the promise without waiting for it:
// app/layout.jsx
import UserProvider from '@/app/providers/user-provider';
import { getUser } from '@/lib/user';
export default function RootLayout({ children }) {
const userPromise = getUser();
return (
{children}
);
}
Uses client components. use() To terminate a promise:
// app/ui/profile.jsx
'use client';
import { use, useContext } from 'react';
import { UserContext } from '@/app/providers/user-provider';
export default function Profile() {
const userPromise = useContext(UserContext);
if (!userPromise) {
throw new Error('Profile must be used within UserProvider');
}
const user = use(userPromise);
return Welcome, {user.name}
;
}
Wrap the ingredients. Suspense For loading conditions:
// app/dashboard/page.jsx
import { Suspense } from 'react';
import Profile from '@/app/ui/profile';
export default function DashboardPage() {
return (
Loading profile...