Imagine a restaurant with two kitchens and one dining room. The back kitchen can prep complicated meals, use all the secret ingredients, and do heavy work. The front kitchen can only handle things that can be safely carried through the dining room doors.
In Next.js, Server Components are like the back kitchen: they can fetch data, read secrets, and do expensive work before anything reaches the browser. Client Components are like the dining room: they run in the browser, can respond to clicks, and are interactive. The tricky part is the door between them — only certain things can be passed across safely.
What the component boundary really means
The Server vs Client Components split is not about where code is written; it is about where it runs. By default in modern Next.js, components are Server Components unless you mark a file with 'use client', which makes that file and its imports part of the client bundle.
This matters because it changes what each side is allowed to do. Server Components can access server-only resources and never ship their code to the browser, while Client Components can use state, effects, and event handlers. The boundary model is the core interview concept: data can flow from server to client, but interactive behavior must live on the client side.
The common trap is thinking 'use client' just enables hooks. It also creates a bundle boundary and a serialization boundary: props passed from server to client must be transferable as plain data, not live functions or class instances.
Step 1: Find the server/client boundary
The boundary is created by the first file that says 'use client'. Everything above that boundary in the tree can stay on the server, and everything inside that module graph becomes client-side code.
That means you should ask two questions for every component:
Does this need browser-only features like state, effects, or event handlers?
Does this need server-only capabilities like secure data access or fast pre-rendering?
If the answer is “browser-only,” mark it client. If the answer is “server-only” or “static/data-heavy,” keep it server. A lot of performance wins come from pushing the boundary down as far as possible.
Step 2: Pass only serializable data across
Props crossing from Server Components to Client Components must be serializable. In practice, that means simple JSON-like values: strings, numbers, booleans, null, arrays, and plain objects.
You cannot pass things that only exist in memory on the server, such as:
Functions
Class instances
DOM nodes
Symbols
Non-serializable complex objects
This rule exists because the server must describe the client tree in a form the browser can reconstruct. Interviewers often test whether you understand that a Server Component can compute a value and pass the result, but cannot pass a callback directly into the browser.
Step 3: Compose across the boundary intentionally
A powerful pattern is interleaving: render a Server Component as a child of a Client Component, or vice versa through props and slots. The most common safe direction is to let the server fetch and prepare data, then pass that data into a small interactive client wrapper.
Another useful pattern is to pass Server Components as children into a Client Component. The client wrapper handles interaction and layout, while the server child provides data-rich content that never ships as client code.
This keeps your interactive surface small and preserves server rendering benefits. The mistake is to turn an entire page into a client component just because one button needs state.
Step 4: Stream with Suspense for progressive rendering
With Streaming SSR, the server does not have to wait for every piece of data before sending HTML. It can send the shell of the page first, then stream in slower parts as they become ready.
Suspense boundaries control this progression. A boundary lets you show a fallback for a slow subtree while the rest of the page renders immediately. In interview terms, this is how Next.js avoids a blank screen when one data fetch is slow.
This matters most when you combine Server Components with asynchronous data fetching. The fast parts can render right away, the slow parts can stream later, and the browser can become useful earlier.
Worked example: server data plus a client interaction shell
Suppose you are building a product page. The page should fetch product details on the server, but a quantity selector needs browser interactivity.
A good shape is:
Server Component: fetch product data and render the description, reviews, and a client wrapper.
Client Component: manage quantity state and handle the add-to-cart click.
Suspense: wrap a slow reviews section so the product header appears first.
This preserves server rendering for the data-heavy parts while keeping only the interactive widget on the client. If the reviews are slow, the page still streams the header and buy box immediately, instead of waiting for everything to finish.
// Server Component
import AddToCart from './AddToCart';
export default async function ProductPage() {
const product = await getProduct();
return <AddToCart productName={product.name} price={product.price} />;
}
// Client Component
'use client';
import { useState } from 'react';
export default function AddToCart({ productName, price }) {
const [qty, setQty] = useState(1);
return <button onClick={() => add(price, qty)}>{productName} x {qty}</button>;
}
Worked example: server children inside a client shell
A client sidebar can provide interactivity, while the main content remains a Server Component. This is useful when the layout needs toggles, tabs, or collapsible UI, but the content itself is mostly server-rendered.
The important detail is that the client shell receives the server-rendered subtree as children, not as an arbitrary function. The server still owns data fetching and can stream the content independently.
This is a classic interview pattern because it proves you understand that client components do not have to own the whole subtree. They can be a thin interactive shell around server-rendered content.
'use client';
export function Shell({ children }) {
return <div className="sidebar-layout">{children}</div>;
}
// Server Component usage
<Shell>
<Report />
</Shell>
Common traps interviewers look for
The biggest trap is overusing 'use client'. If you place it too high in the tree, you pull unrelated code into the browser bundle and lose server-only advantages.
Other common mistakes:
Passing a function, class, or complex object across the server/client boundary
Thinking Client Components can directly access server secrets
Using hooks in a Server Component and assuming it will work
Forgetting that a Client Component import graph becomes client-side too
Ignoring Suspense and making the whole page wait for the slowest fetch
Interviewers often ask why a component fails after adding 'use client'. The right diagnosis is usually boundary-related: either the component now needs serialization-safe props, or it accidentally dragged server-only code into the client bundle.