React Server Components run on the server and stream HTML to the client. They can read data and use server-only libraries directly, but they cannot use state, effects or browser APIs. The 'use client' directive marks the file where the tree becomes interactive, and that boundary has rules about what can cross it.
Before you start
You should be comfortable with React components, props and hooks, and know the difference between rendering on the server and running in the browser. This article assumes a framework that supports the server/client split.
Step-by-step walkthrough
Step 1: Fetch in server components
A server component can be async, so it awaits data and renders the result without a loading state or an effect. Because it never ships to the browser, it can import database clients and secrets that a client bundle must not contain.
Step 2: Mark interactive leaves with ‘use client’
Put 'use client' at the top of the file that needs useState, event handlers or effects. Everything it imports becomes part of the client bundle, so keep the boundary as low in the tree as possible and pass server-rendered children into client wrappers.
Step 3: Keep props serializable
Props crossing into a client component must be serializable: plain data, arrays, and possibly promises. Functions, class instances and symbols cannot cross unless they are server actions. Design the boundary around data, not callbacks.
Worked scenario
The server component fetches and renders, and a small client leaf handles the interaction.
// PostList.tsx — server component (no directive)
import { LikeButton } from './LikeButton';
export async function PostList() {
const posts = await getPosts();
return (
<ul>
{posts.map((post) => (
<li key={post.id}>
{post.title}
<LikeButton id={post.id} />
</li>
))}
</ul>
);
}Walk through the example
PostList awaits getPosts() on the server and emits the list markup. LikeButton is a client component because it reacts to clicks, and it receives only the serializable id. The server component never enters the client bundle, while the button does, which is the intended split.
Common mistake
Calling useState or useEffect in a server component, which throws, or passing a function prop across the boundary, which is not serializable. Another mistake is putting 'use client' at the app root, which turns the whole tree into a client bundle and loses the benefit.
Verify the behavior
Build the app and inspect the client bundle to confirm the data-fetching dependencies are absent. Add a client component far down the tree and assert it still hydrates. Pass a non-serializable value and read the framework’s error, then replace it with plain data.
Interview exercise
A server component needs a click handler on a row. Where does the handler go?
Answer and reasoning
The handler must live in a client component, because event handlers require the browser. Keep the server component for the data and rows, and extract the interactive part into a small 'use client' child that receives serializable props such as an id. This keeps the boundary narrow and the bundle small.
Continue learning
Compare fetching patterns in React request races and React suspense boundaries. Read the React Server Components guide and try the React interview questions.