React Server Components in Next.js, explained simply
A plain-English guide to what runs on the server, what runs in the browser, and when to use `use client` in the Next.js App Router.
Start with the default
In the Next.js App Router, pages and layouts are Server Components by default. That means the server can fetch data, build the UI, and send the result to the browser before the interactive parts of the page are loaded. This is the starting point, not an advanced option.
That default is important because it changes how you think about a page. You do not begin by asking how much JavaScript should run in the browser. You begin by asking what can stay on the server, and then you add client-side behavior only where the user truly needs interaction.
What a Server Component is
A Server Component runs in a server environment. In practical terms, this makes it a good place for reading data, using secrets safely, and building UI that does not need browser-only features. Because the work happens on the server, the browser does not need to download JavaScript for every part of that component tree.
This is one of the main reasons Server Components matter. They help reduce the amount of JavaScript sent to the client. That usually means less work for the browser and a cleaner separation between data work and interactive UI work. The user still gets the page content, but the browser only hydrates the parts that really need to be interactive.
// app/products/[id]/page.tsx
import { getProduct } from "@/lib/products";
type ProductPageProps = {
params: Promise<{ id: string }>;
};
export default async function ProductPage({ params }: ProductPageProps) {
const { id } = await params;
const product = await getProduct(id);
return (
<main>
<h1>{product.name}</h1>
<p>{product.description}</p>
<p>${product.price}</p>
</main>
);
}This is a Server Component because there is no use client directive. The data is fetched on the server, and the browser receives the rendered result.
- Fetch data from a database or API close to render
- Keep secrets and server-only logic off the client
- Render static or data-heavy UI without adding client bundle weight
- Pass ready-to-display data down to smaller interactive components
What a Client Component is
A Client Component is the part of the UI that needs browser features. In Next.js, you mark that boundary with use client at the top of the file. You use it when a component needs state, event handlers, effects, or browser APIs like window, localStorage, or focus management.
The key idea is that use client is not something you spread everywhere. It is a boundary. Once you mark a file as client code, you are choosing to send that part of the tree to the browser. That is why small client boundaries usually lead to better results than turning an entire page into a Client Component.
// app/products/[id]/add-to-cart-button.tsx
"use client";
import { useState } from "react";
type AddToCartButtonProps = {
productId: string;
};
export default function AddToCartButton({ productId }: AddToCartButtonProps) {
const [isAdding, setIsAdding] = useState(false);
async function handleClick() {
setIsAdding(true);
await fetch("/api/cart", {
method: "POST",
body: JSON.stringify({ productId, quantity: 1 }),
});
setIsAdding(false);
}
return (
<button onClick={handleClick} disabled={isAdding}>
{isAdding ? "Adding..." : "Add to cart"}
</button>
);
}This belongs on the client because it uses state and a click handler.
- Use Client Components for forms, menus, filters, and buttons
- Use them when you need
useState,useEffect, or DOM events - Keep the boundary as small as possible
- Pass plain serializable data from the server into the client part
A simple pattern that works well
A strong default pattern is this: let the page fetch data on the server, render the layout on the server, and isolate only the interactive pieces on the client. For example, a product page can fetch the product, reviews, and recommendations on the server, while the quantity selector and add-to-cart button stay on the client.
This pattern stays easy to read because each part has a clear job. The Server Component owns data and page composition. The Client Component owns interaction. That split also makes it easier to test changes, reason about performance, and avoid sending extra JavaScript to the browser.
// app/products/[id]/page.tsx
import { getProduct, getReviews } from "@/lib/products";
import AddToCartButton from "./add-to-cart-button";
type ProductPageProps = {
params: Promise<{ id: string }>;
};
export default async function ProductPage({ params }: ProductPageProps) {
const { id } = await params;
const [product, reviews] = await Promise.all([
getProduct(id),
getReviews(id),
]);
return (
<main>
<h1>{product.name}</h1>
<p>{product.description}</p>
<section>
<h2>Reviews</h2>
<ul>
{reviews.map((review) => (
<li key={review.id}>{review.comment}</li>
))}
</ul>
</section>
<AddToCartButton productId={product.id} />
</main>
);
}This is the mental model to keep: fetch and compose on the server, interact on the client.
Mistakes that cause confusion
The most common mistake is adding use client too high in the tree. When that happens, pages that could have stayed mostly server-rendered become large client bundles. The second common mistake is trying to use browser APIs inside a Server Component. If the code needs window, local component state, or event handlers, that part belongs on the client.
Another common issue is passing the wrong data into a Client Component. In Next.js, props sent from the server to the client must be serializable. That means plain data is fine, but server-only objects and direct function props are not. If the data shape is simple and the client boundary is small, this usually stays easy to manage.
// Incorrect: browser-only code inside a Server Component
export default async function Page() {
const width = window.innerWidth;
return <p>Screen width: {width}</p>;
}// Correct: move browser-only logic into a Client Component
"use client";
import { useEffect, useState } from "react";
export default function ViewportWidth() {
const [width, setWidth] = useState(0);
useEffect(() => {
setWidth(window.innerWidth);
}, []);
return <p>Screen width: {width}</p>;
}// Good: pass serializable data from server to client
<AddToCartButton productId={product.id} />// Avoid: functions and server-only objects as client props
<AddToCartButton product={product} saveToDb={saveToDb} />- Putting
use clientat the top of a whole page without a real reason - Fetching data again on the client when the server already has it
- Using
window,document, or effects in a Server Component - Passing non-serializable props into Client Components
How to decide quickly
When you are not sure where a component belongs, ask a simple question: does this part need the browser to do its job? If the answer is no, keep it on the server. If the answer is yes because of interaction, state, or browser APIs, move only that part to the client.
This rule is not perfect, but it is good enough for most product work. It also pushes the codebase toward smaller client bundles and clearer component boundaries. Over time, that usually leads to pages that are easier to maintain and easier to reason about.
- Keep data access and page composition on the server
- Move only interactive UI to the client
- Start small and expand the client boundary only when needed
- Prefer clarity over clever component splitting
The simple takeaway
Server Components are not magic. They are just a better default for a large part of modern web UI. They let the server do the work it is good at, and they let the browser focus on the pieces that need user interaction.
If you remember one rule, make it this: default to the server, then add use client only where the user needs it. That keeps the architecture simple, the bundle smaller, and the code easier for the next engineer to understand.