Server vs Client Components in Next.js: Where to Draw the Boundary
How to decide what runs on the server and what ships to the browser in the Next.js App Router, with patterns for forms, interactivity and passing data across the boundary.
React Server Components change a question that used to have one answer. Before, every component ran in the browser (and was usually also rendered once on the server). Now each component runs in exactly one place by default, and you choose where. Getting that choice right affects bundle size, data fetching, security and SEO.
What actually differs
A Server Component runs only on the server. Its code is never sent to the browser. It can be async, read from a database or the file system, and use secrets. It cannot hold state, run effects or attach event handlers.
A Client Component is marked with "use client" at the top of the file. It is rendered on the server for the first HTML as well, but its code is also shipped to the browser, where it hydrates and becomes interactive. It can use useState, useEffect, event handlers and browser APIs.
The default in the App Router is the server. You pay the client cost only for the files you opt in.
A decision rule that works
Ask what the component needs:
- Does it need state, effects, event handlers or browser APIs? Client Component.
- Does it fetch data, read secrets or touch the database? Server Component.
- Neither? Server Component, because it ships no JavaScript.
Most pages end up as a server-rendered shell with a few small interactive islands: a mobile menu, a form, a video player, a progress tracker.
Push "use client" to the leaves
The directive applies to the file and everything it imports. Putting it on a page or layout drags the whole subtree into the client bundle. Put it on the smallest component that needs it.
// Server Component — no directive, ships no JS of its own
export default async function CoursePage({ params }: Props) {
const { slug } = await params;
const course = await getCourse(slug);
return (
<article>
<h1>{course.title}</h1>
<CourseOutline chapters={course.chapters} />
<EnrollButton courseId={course.id} /> {/* only this is interactive */}
</article>
);
}"use client";
import { useState } from "react";
export function EnrollButton({ courseId }: { courseId: string }) {
const [pending, setPending] = useState(false);
return (
<button disabled={pending} onClick={() => { setPending(true); /* ... */ }}>
Enroll
</button>
);
}The heading and outline are plain HTML in the response. Only the button's code reaches the browser.
Passing data across the boundary
Props passed from a Server Component to a Client Component must be serializable: strings, numbers, booleans, arrays and plain objects, Date values, and Server Actions. You cannot pass functions, class instances or database documents with methods.
This matters with Mongoose. A document returned by find() is not a plain object. Use .lean() and, if the data includes ObjectId or Date fields you want as strings, convert them before passing them down:
const docs = await Post.find({ published: true }).lean();
const posts = docs.map((d) => ({
slug: d.slug,
title: d.title,
createdAt: d.createdAt.toISOString(),
}));Server components can render client components, and the reverse needs care
A Server Component can import and render a Client Component. A Client Component cannot import a Server Component — but it can receive one as children or another prop, because the server renders it first and passes the result through.
"use client";
export function Collapsible({ title, children }: { title: string; children: React.ReactNode }) {
const [open, setOpen] = useState(false);
return (
<section>
<button aria-expanded={open} onClick={() => setOpen(!open)}>{title}</button>
{open && children}
</section>
);
}children here can be a heavy, server-rendered tree. The wrapper adds the toggle; the content stays on the server.
Data fetching: stop fetching in useEffect
The old pattern was to render an empty shell and fetch in an effect. With Server Components the page can fetch before it renders, and the HTML that reaches the browser (and search engines) already contains the content.
Compare the two outcomes for an article page:
- Client fetch: the initial HTML contains "Loading..."; content appears after JavaScript runs and an API call returns. Crawlers that do not execute JavaScript fully see nothing useful.
- Server fetch: the initial HTML contains the article. Hydration adds interactivity afterwards.
Client-side data fetching still has its place — for data that changes after load or depends on user interaction, a library such as TanStack Query is a good fit. It is a poor fit for content that should be indexed.
Context providers and third-party libraries
Context requires a client boundary. Wrap providers in a small client component and render it from the root layout, passing children through, so the pages beneath remain Server Components:
"use client";
export function Providers({ children }: { children: React.ReactNode }) {
const [client] = useState(() => new QueryClient());
return <QueryClientProvider client={client}>{children}</QueryClientProvider>;
}Libraries that use hooks or browser APIs need a "use client" file around them. Wrapping once, in your own file, is cleaner than sprinkling the directive through the codebase.
Common mistakes
- Marking a whole page
"use client"because one widget neededuseState. - Importing a server-only module (database, secrets) into a client file. The
server-onlypackage turns that mistake into a build error. - Passing non-serializable values as props.
- Forgetting that
NEXT_PUBLIC_variables are public. - Fetching indexable content in
useEffect.
Why it matters for large applications
On a content-heavy product such as a learning platform — lesson lists, chapter outlines, descriptions — most of the screen is read-only. Rendering that on the server and keeping interactivity to the player, the quiz and the checkout controls keeps pages light on low-end phones and slow connections. For how this fits a wider layout, see Structuring a Next.js App Router Project for Production, and for how the same ideas show up in a real product, the E2A Learning case study.
Related articles
- Structuring a Next.js App Router Project for Production
A practical layout for Next.js App Router projects — routes, data access, shared components and configuration — and the reasoning behind each boundary.
- A Practical SEO Setup for the Next.js App Router
Metadata, canonical URLs, sitemaps, robots rules, structured data and Open Graph images in the Next.js App Router — implemented once, in a way that scales as you add pages.
- One Backend, Two Clients: Architecture for a Next.js Web App and an Expo Mobile App
How to share an API, validation schemas and data-fetching patterns between a Next.js web app and an Expo / React Native app without duplicating business rules.