The moment you open a project with the Next.js App Router, you are living in the world of Next.js Server Components by default. The old model we were used to — "every React component runs in the browser" — has given way to a two-part architecture where some components render on the server and only some ship to the browser. In this article I explain the real difference between Server Components and Client Components, how they work together, and which one to reach for in day-to-day work, with concrete examples.
What are Server Components and Client Components?
The distinction comes down to where a component runs.
- Server Component (RSC): Runs only on the server. Its output is sent to the browser as HTML plus a special serialized payload; the component's own JavaScript is not shipped to the client. It can access the database, the file system, or secret API keys directly.
- Client Component: Rendered once on the server, then "hydrated" (made interactive) in the browser.
useState,useEffect, event listeners and browser APIs all live here.
In the App Router, every component is a Server Component by default. To turn a component into a Client Component, you add the "use client" directive at the top of the file.
What does "use client" actually do?
A common misconception is that a file marked "use client" runs "only in the browser." In reality that component is still rendered on the server on the first request; the directive's job is to mark the client boundary that starts in that file. From that boundary onward, the component and the things it imports are included in the client bundle.
"use client";
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}
The directive must sit at the very top of the file, before any imports. Once you cross into a Client Component boundary, every child component rendered from inside it is also evaluated on the client.
Which one should you choose, and when?
The practical rule is simple: leave the default as a Server Component, and only switch to Client when you need interactivity or a browser API.
Prefer a Server Component when:
- You fetch data from a database or an API (directly with
async/await). - You use keys, tokens or connection strings that must stay secret.
- You want to keep a heavy dependency (a Markdown parser or a date library, say) out of the client bundle.
- The content is largely static with no interaction.
Prefer a Client Component when:
- You need state (
useState,useReducer) or lifecycle (useEffect). - You use event listeners such as
onClickoronChange. - You need browser-specific APIs like
window,localStorageorIntersectionObserver. - You rely on a client-only library (many animation or map components, for instance).
Using both together: the composition pattern
The real power is in nesting the two. The common, recommended pattern is to fetch data on the server and pass it to a small Client Component as a prop. That way you keep the interactive part small and leave the heavy lifting on the server.
// app/page.tsx (Server Component)
import Counter from "./counter";
export default async function Page() {
const data = await getData(); // runs on the server
return (
<main>
<h1>{data.title}</h1>
<Counter /> {/* client boundary */}
</main>
);
}
One important detail: you cannot import and render a Server Component inside a Client Component, but you can pass it as children. This lets you show Server Component content inside a Client wrapper (a theme provider, for example):
// Client Component
"use client";
export default function Panel({ children }) {
return <div className="panel">{children}</div>;
}
// Server Component
<Panel>
<ServerContent /> {/* passed as children, stays on the server */}
</Panel>
Common mistakes
- Putting
"use client"on everything. This throws away all the benefits of RSC; the bundle grows and the first load slows down. Push the directive as close as possible to the leaf (the innermost interactive) component. - Trying to use
useStateor an event handler in a Server Component. This produces a build/runtime error; you have to make the component a Client Component. - Moving a secret key into a Client Component. Anything that lands in the client bundle is visible to everyone. Secrets must stay in Server Components or server-side code only.
- Writing an
asyncClient Component. Server Components can beasyncto fetch data; to fetch data in Client Components, useuseEffector a library such as SWR.
Frequently Asked Questions
Did Server Components replace getServerSideProps?
Largely yes. The App Router no longer has getServerSideProps or getStaticProps; you fetch data directly with await inside an async Server Component. Caching and revalidation behaviour is then controlled through the fetch options.
Can a Client Component take a Server Component as a child?
Yes. A Client Component cannot import and render a Server Component directly, but it can receive one through children or another prop. This is the standard way to combine interactive wrappers with server-rendered content.
Why doesn't useState work in a Server Component?
Because useState needs state that lives in the browser and persists across renders; a Server Component has no client-side lifecycle. When you need state or interactivity, split that piece out into a Client Component.
Want to structure your Next.js architecture correctly? I can help you build a fast, maintainable app that balances Server and Client Components well. Get in touch.