What you'll learn
Quick Answer
React Server Components (RSCs) run only on the server and send rendered output to the browser, never their code. They can beasyncand read a database or filesystem directly, adding zero JavaScript to the bundle. Components that need state, effects, or event handlers opt into the browser with the"use client"directive. In frameworks like the Next.js App Router, every component is a Server Component unless marked otherwise.
Two kinds of components now
Before RSCs, every React component was the same kind of thing: it shipped to the browser as JavaScript and ran there. Server-side rendering existed, but the server only produced HTML and then sent the same component code to the client to hydrate.
React Server Components split components into two categories:
- Server Components - run on the server, at build time or per request, produce a serialized description of UI, and send that. Their code never reaches the browser. This is the default in the Next.js App Router.
- Client Components - the components you already know. They render on the server for the initial HTML and ship to the browser to become interactive. You opt in by putting
"use client"at the top of the file.
The goal is to make "no JavaScript" the default for the large share of a typical UI that just displays data, and to pay for JavaScript only where there is real interactivity.
What a Server Component can and cannot do
A Server Component can be an async function, which changes how you fetch data:
// app/page.tsx - a Server Component
import { getUser } from './db';
export default async function Profile() {
const user = await getUser(); // runs on the server
return <h1>Welcome, {user.name}</h1>;
}
No useEffect, no loading state, no client-side fetch waterfall. The function reads data - a database query, a file, an internal API with a secret key - and returns UI. The credentials and the query never leave the server.
What a Server Component cannot do: use useState, useReducer, useEffect, or any hook that depends on a persistent component instance; attach event handlers such as onClick; or touch browser APIs like window and localStorage. Import useState into one and the build fails with: "You're importing a module that depends on useState into a React Server Component module. This API is only available in Client Components."
The "use client" boundary
"use client" is a directive - a string literal at the very top of a file, before the imports:
"use client";
import { useState } from 'react';
export default function Counter({ start }) {
const [count, setCount] = useState(start);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
It marks a boundary, not just one file. Every module that file imports, and every component rendered inside it, becomes part of the client bundle from that point down - unless a Server Component is passed into it as a prop.
Two things people misread:
"use client"does not mean "client-only". A Client Component still renders on the server for the initial HTML - confirmed by finding its text in the prerendered HTML of a build. It just also runs in the browser.- You do not put
"use client"on every component. Put it on the leaves - the actual interactive widgets - and keep pages and layouts as Server Components. Marking a top-level layout as a client component drags the entire tree below it into the browser bundle.
What actually ships to the browser
Here is the concrete payoff, confirmed by inspecting a production build. Put a unique string inside a Server Component's data function and build the app:
- The string appears under
.next/server/- in the server-rendered HTML and the RSC payload. - It appears in none of the JavaScript files under
.next/static/that the browser downloads.
The same string placed inside a Client Component does turn up in a .next/static/chunks/ file. Server Component code - including any library it imports, such as a date formatter or a markdown parser - is simply absent from the bundle. A heavy dependency used only for server rendering costs the user nothing.
What the browser receives for a Server Component is the RSC payload: a compact, streamed description of the rendered output - element types, props, and slots where Client Components go - not JSX and not plain HTML. React uses it to build the tree on the client and to reconcile after navigation without re-running any server code.
Passing props across the boundary
Props passed from a Server Component to a Client Component cross a serialization boundary, so they must be serializable: strings, numbers, booleans, plain objects and arrays of those, Date, Map, Set, and JSX elements. What cannot cross:
- Functions, including event handlers. This build error is one every beginner hits:
The fix is to define the handler inside the Client Component, not pass it in. Server Actions - functions markedError: Event handlers cannot be passed to Client Component props. {onClick: function onClick} If you need interactivity, consider converting part of this to a Client Component."use server"- are the one deliberate exception and can be passed as props. - Class instances with methods, and anything else carrying behaviour rather than data.
Data flows one way: Server Component to Client Component, through props. A Client Component cannot import a Server Component and render it directly - the import would pull server code into the browser bundle.
Server components inside client components
The pattern that keeps this composable: a Client Component accepts children (or any JSX prop) and a Server Component fills the slot.
// ClientShell.tsx
"use client";
export default function ClientShell({ children }) {
const [open, setOpen] = useState(true);
return open ? <div>{children}</div> : null;
}
// page.tsx - a Server Component
export default function Page() {
return (
<ClientShell>
<ServerInfo /> {/* still a Server Component */}
</ClientShell>
);
}
ServerInfo renders on the server; its already-rendered output is slotted into ClientShell as children. ClientShell only positions that content - it never sees the server code. Confirmed in a build: the server component's marker string stays out of the client chunks even though its parent in the tree is a Client Component.
This is how you wrap server-rendered content in interactive shells - tabs, accordions, modals - without losing the zero-bundle benefit.
