Suggest an editImprove this articleRefine the answer for “React server components (rsc)”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**React Server Components (RSC)** are components that run only on the server per request, sending HTML to the browser without shipping their own JavaScript. They can access Node.js resources like databases or the file system directly. ```tsx async function UserProfile({ userId }: { userId: string }) { const user = await db.user.findUnique({ where: { id: userId } }); return <div><h1>{user.name}</h1><p>{user.email}</p></div>; } ``` **Key point:** RSC have zero JS bundle cost. Add `"use client"` only when you need state, event handlers, or browser APIs.Shown above the full answer for quick recall.Answer (EN)Image**React Server Components (RSC)** render on the server per request, sending HTML or client component references to the browser without shipping their own JavaScript. ## Theory ### TL;DR - RSC is like a restaurant kitchen: you get the finished dish (HTML) at your table (browser), no kitchen tools (JS code) come with it - Main difference: RSC have zero JS bundle; client components ship JS for interactivity - RSC can render client components; client components cannot render server components directly (but can accept them as `children`) - Default in Next.js App Router - no directive needed; add `"use client"` only when you need interactivity ### Quick example ```tsx // app/user/[id]/page.tsx - Server Component (default in Next.js) async function UserPage({ params }: { params: { id: string } }) { // Direct DB access - no API route needed const user = await db.user.findUnique({ where: { id: params.id } }); return ( <div> <h1>{user.name}</h1> <p>{user.email}</p> <LikeButton userId={params.id} /> {/* Client component - ships as JS */} </div> ); } // UserPage JS never reaches the browser. LikeButton JS does. ``` `UserPage` runs once per request on the server. The HTML for `h1` and `p` streams directly. `LikeButton` gets bundled as client JS because it needs `useState`. ### Key difference RSC execute in Node.js. They can call Prisma, read from `fs`, or use `headers()` from `next/headers`. Client components run in the browser (and during SSR), so they get Web APIs like `window` and `document`. The practical result: static parts of your UI ship zero JavaScript. In production Next.js apps, this can cut the JS bundle by 90% or more for pages that are mostly data display. ### Server vs client: what each can do | Feature | Server Component | Client Component | |---|---|---| | `"use client"` directive | No | Yes | | Runs on | Server only | Server (SSR) + browser | | JS bundle contribution | Zero | Included | | `useState`, `useEffect` | No | Yes | | Event handlers (`onClick`) | No | Yes | | `async` component | Yes | No | | Direct DB / filesystem access | Yes | No - via API only | | Browser APIs (`window`, `document`) | No | Yes | ### Composition rules ``` Server Component → can render Server or Client components ✅ Client Component → can render Client components only ❌ Client Component → can accept Server Component as children prop ✅ ``` The `children` pattern is the escape hatch when you need a client shell around server-rendered content: ```tsx // layout.tsx - Server Component passes children to a Client Component export default function Layout({ children }: { children: React.ReactNode }) { return ( <ClientSidebar> {children} {/* This can be an entire Server Component tree */} </ClientSidebar> ); } ``` React evaluates `children` on the server before passing the result to `ClientSidebar`. The client component receives already-rendered output, not the server component itself. ### How RSC works internally Next.js uses a React Server Component Runtime in Node.js. During a request, it traverses the component tree once, serializing RSC output into a binary RSC Payload via `react-server-dom-webpack/server`. This payload contains rendered HTML for server components and module references for client components. The browser receives the stream, renders client component slots with the referenced JS, and skips re-running RSC code entirely. No hydration happens for server components. This is different from classic SSR where every component re-runs in the browser to attach event listeners. ### When to use - Data fetching for a product page, user profile, or dashboard: RSC (direct DB, zero client JS) - Interactive buttons, forms, modals: client component nested inside RSC - Secrets like API keys or database credentials: RSC (never reaches the browser) - Markdown rendering, syntax highlighting, heavy parsing libraries: RSC (libraries stay on server) - WebSockets, canvas, localStorage: client component (browser-only APIs) ### Streaming with Suspense ```tsx // app/dashboard/page.tsx import { Suspense } from 'react'; async function SlowAnalytics() { const data = await fetchAnalytics(); // 2s delay return <Chart data={data} />; } async function RecentUsers() { const users = await db.user.findMany({ take: 5 }); return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>; } export default function Dashboard() { return ( <div> <h1>Dashboard</h1> <Suspense fallback={<p>Loading analytics...</p>}> <SlowAnalytics /> </Suspense> <Suspense fallback={<p>Loading users...</p>}> <RecentUsers /> </Suspense> </div> ); } // Shell HTML streams immediately. Both components fetch in parallel. ``` Each `Suspense` boundary is independent. The browser renders the shell instantly, then fills in chunks as they arrive. Time to Interactive drops because the page is usable before all data loads. ### Common mistakes **Fetching data in a client component when the RSC already has it:** ```tsx // Wrong - double fetch, exposes the API endpoint "use client"; async function Likes({ userId }) { const count = await api.likes(userId); return <span>{count}</span>; } // Right - fetch in RSC, pass as prop async function LikeSection({ userId }) { // Server Component const count = await db.likes.count({ where: { userId } }); return <LikeDisplay count={count} />; // Client component gets data, not a fetch } ``` **Importing browser-only modules in RSC:** ```tsx // Wrong - throws at build time import { useEffect } from 'react'; // In a Server Component import { Chart } from 'chart.js'; // Browser-only library ``` Move these to a `"use client"` file or use `dynamic(() => import(...), { ssr: false })`. **Putting `"use client"` too high in the tree:** ```tsx // Wrong - this makes the entire subtree a client bundle "use client"; export default function Page() { // Should be a Server Component return <div><ProductList /></div>; // ProductList becomes client too } ``` Keep `"use client"` at the leaves - only on the interactive parts. Pulling a parent component into the client bundle pulls its entire subtree with it. **Trying to use state in RSC:** ```tsx // Wrong - RSC are stateless, single render per request function Counter() { const [count, setCount] = useState(0); // Error: hooks not supported in Server Components return <p>{count}</p>; } ``` ### Real-world usage - Next.js 14+ App Router: pages and layouts are RSC by default. Vercel's commerce demo uses RSC for 100+ product listing pages with zero JS for the listing itself. - PayloadCMS and Waku use RSC for admin dashboards with direct Prisma access. - React 19 supports RSC outside Next.js via `react-server-dom-webpack` in Express servers. - The pattern "RSC for data, client component for interaction" maps directly to how most production apps are structured today. ### Follow-up questions **Q:** Can an RSC access cookies or auth headers? **A:** Yes. Use `cookies()` and `headers()` from `next/headers`. These are server-only and unavailable in client components. **Q:** What format does the RSC Payload use? **A:** It is a binary stream with module references, something like `[ref:0,"div",[ref:1,"h1","John Doe"]]`. The client resolves these via a webpack module map to render client component slots. **Q:** How do you test a Server Component? **A:** Use `react-test-renderer` to extract the static tree. No `act()` is needed because RSC have no state. For integration tests, `@testing-library/react` with a mocked async component works fine. **Q:** What changes in React 19 vs a Next.js-specific RSC setup? **A:** React 19 exposes the `use` primitive so client components can read server data from a Promise. Next.js 15 adds Turbopack which speeds up RSC builds by around 10x. The core RSC model stays the same. **Q:** Explain RSC flight graph optimization and tree hoisting. **A:** Client components get hoisted up the tree and deduplicated in the bundle. The flight graph (the serialized RSC payload) merges duplicate subtrees server-side, so repeated components like nav or footer do not ship multiple times. This deduplication happens inside `@react-server-dom-webpack` during serialization. ## Examples ### Basic: Server Component with direct DB access ```tsx // app/products/[id]/page.tsx import { db } from '@/db'; import { eq } from 'drizzle-orm'; import { products } from '@/db/schema'; import AddToCart from '@/components/AddToCart'; // "use client" async function ProductPage({ params }: { params: { id: string } }) { const product = await db.query.products.findFirst({ where: eq(products.id, params.id), with: { variants: true } }); return ( <article> <h1>{product.name}</h1> <p>${product.price}</p> <AddToCart productId={product.id} /> {/* Only this ships JS */} </article> ); } ``` `ProductPage` ships zero JavaScript. The DB query runs on the server. `AddToCart` is the only component in the JS bundle because it needs `onClick`. ### Intermediate: Streaming dashboard with parallel fetches ```tsx // app/dashboard/page.tsx import { Suspense } from 'react'; async function Analytics() { const data = await fetchAnalyticsReport(); // Slow external API return <BarChart data={data} />; } async function RecentOrders() { const orders = await db.select().from(ordersTable).limit(10); return ( <ul> {orders.map(o => ( <li key={o.id}>{o.product} - ${o.total}</li> ))} </ul> ); } export default function Dashboard() { return ( <> <h1>Dashboard</h1> <Suspense fallback={<div>Loading analytics...</div>}> <Analytics /> </Suspense> <Suspense fallback={<div>Loading orders...</div>}> <RecentOrders /> </Suspense> </> ); } ``` Both components fetch in parallel. The page shell renders immediately. Each section streams in when its data is ready. No JavaScript waits for either fetch on the client. ### Advanced: Heavy libraries kept server-side ```tsx // app/blog/[slug]/page.tsx import { marked } from 'marked'; // ~50KB - stays on server import hljs from 'highlight.js'; // ~100KB - stays on server import ShareButton from '@/components/ShareButton'; // "use client" - needed for clipboard API async function BlogPost({ params }: { params: { slug: string } }) { const post = await getPostBySlug(params.slug); // Both libraries run on the server and never reach the browser const html = marked(post.content); return ( <article> <h1>{post.title}</h1> <div dangerouslySetInnerHTML={{ __html: html }} /> <ShareButton url={`/blog/${params.slug}`} /> {/* ~2KB of JS */} </article> ); } ``` `marked` and `highlight.js` together are around 150KB. In a CSR app, both ship to every visitor. Here they run once on the server per request. The client receives HTML and roughly 2KB of `ShareButton` JS. That is the entire bundle for this page.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.