The biggest mental shift in Next.js is that not every component needs to run in the browser.
Beginners often add "use client" too quickly because browser interactivity feels familiar, but that can throw away many of the framework benefits.
Professionals use server components by default and introduce client components only where state, effects, or browser APIs are truly needed.
This lesson matters because the server-versus-client boundary affects bundle size, security, and data flow all at once.
Many learners think server components are only for data fetching, but the idea is broader. If a piece of UI can be prepared without browser-only APIs or live client state, it can often remain on the server. That means less JavaScript is sent to the user.
A common beginner mistake is adding "use client" at the top of a parent component and accidentally turning a large part of the tree into client-side code when only one small button needed it.
Professionals often keep page shells, lists, and read-only data rendering on the server, then embed smaller client islands for search bars, filters, modals, or live controls. This creates a better balance between speed and interaction.
The question is not "can this be a client component?" The better question is "what is the minimum interactive surface that needs client behavior?" That framing usually produces cleaner code and smaller bundles.
Components are Server Components by default in the App Router. They can read server-side data, use secrets, and send rendered output without adding their implementation to the browser bundle. Use them for pages, data fetching, and non-interactive presentation.
Add the use client directive only when a component needs state, effects, event handlers, or browser APIs. The directive creates a client boundary for that module and its imported dependencies. Keep the boundary close to the interactive control rather than marking an entire page or layout as client code.
Props crossing from server to client must be serializable. Pass small plain values, not database clients, class instances, functions, or secrets. Client Components may render Server Component output through composition, which can preserve server work while allowing an interactive shell.
Server components cannot use browser APIs, event handlers, or React hooks like useState and useEffect. Client components can, but they add bundle weight and hydration cost. Understanding this tradeoff is more important than memorizing the syntax.
The best developers treat the boundary as an architectural decision. It changes how fast the page loads, how easy the code is to test, and what logic stays private on the server.
Render a searchable catalog on the server and isolate only the filter controls as a Client Component. Pass serializable values rather than database objects or functions across the boundary.
Work through this as a controlled engineering exercise rather than a copy-and-paste demo. State the expected result before running anything, keep the input small enough to inspect, and record the important intermediate state. That makes the lesson explain not only what to type, but why the result is trustworthy.
Marking the page use client pulls descendants and dependencies into the browser bundle. Accessing secrets, filesystem APIs, or non-serializable values from the client boundary fails or leaks architecture.
Verification must use evidence that matches the concept. Compare bundle output before and after moving the boundary, inspect server HTML, and test interaction after hydration. Repeat the check after deliberately introducing the failure, then after the fix. The contrast between those runs is the part that turns a definition into practical understanding.
Inspect the production bundle when moving boundaries. A high-level client directive can pull formatting libraries, data helpers, and UI descendants into browser JavaScript. Split providers and interactive widgets narrowly, and avoid importing server-only modules from client code.
Server Components can stream through Suspense boundaries so useful content arrives before slower regions. Choose boundaries that match meaningful UI sections and avoid excessive nested loading flicker. Data cache behavior is separate from component type; define freshness and privacy for every fetch.
Authorization must occur in server data functions, Route Handlers, and Server Actions, not merely in a Server Component parent. Client Components remain untrusted. Test serialization, hydration, disabled JavaScript behavior, bundle size, and direct calls to mutation boundaries.
This split is one of the most practical patterns to understand early.
export default async function ProductsPage() {
const products = await getProducts();
return (
<section>
<h1>Products</h1>
<ProductFilters />
<ProductList products={products} />
</section>
);
}
// ProductFilters.tsx
'use client';
export function ProductFilters() {
return <button>Open filters</button>;
}
Adapt this focused example to a disposable local environment and inspect every result before expanding it.
export default async function CatalogPage() {
const products = await listProducts();
return <Catalog products={products} filters={<ProductFilters />} />;
}
Only the counter code needs to run in the browser.
export default async function ProductPage({ params }) {
const product = await getProduct((await params).id);
return <article>
<h1>{product.name}</h1>
<p>{product.description}</p>
<QuantityPicker productId={product.id} />
</article>;
}
Browser state stays inside the smallest interactive module.
\"use client\";
import { useState } from \"react\";
export function QuantityPicker({ productId }) {
const [quantity, setQuantity] = useState(1);
return <label>
Quantity
<input type=\"number\" min=\"1\" value={quantity}
onChange={event => setQuantity(Number(event.target.value))} />
</label>;
}
Yes. That is a normal pattern. The server component can provide data and structure while the child client component handles interaction.
No. Use them when the browser really needs to manage state, effects, or user interaction. The goal is not zero client code; the goal is intentional client code.
Explore 500+ free tutorials across 20+ languages and frameworks.