BACK TO BLOG

Next.js App Router: Server Components vs Client Components — The Mental Model You Need

April 13, 2026

next.jsreactserver-componentsclient-componentsapp-routerreact-server-componentsuse-clientdata-fetchingweb-development

If you've migrated a Next.js app from the Pages Router to the App Router — or started a new project with it — you've almost certainly hit this moment: "Should this component be a Server Component or a Client Component?" You add "use client" somewhere, things break in a different way, you move it, and eventually it works — but you're not sure why.

That uncertainty is the problem this post solves. After building multiple production apps with the App Router, I've landed on a mental model that makes the Server Component vs Client Component decision predictable and repeatable. No guessing, no trial-and-error.

The Mental Model: Server by Default, Client by Exception

Here's the single most important thing to internalize: in the Next.js App Router, every component is a Server Component by default. You don't opt into Server Components — you opt out of them by adding "use client". This is the opposite of how the Pages Router worked, where everything ran on the client unless you explicitly fetched data server-side with getServerSideProps or getStaticProps.

The mental model is simple: keep everything on the server until you have a specific reason to move it to the client. Those reasons are limited and well-defined.

When You Need "use client"

You add "use client" at the top of a file when the component needs any of these browser-only capabilities:

  • State management — useState, useReducer, or any state library (Zustand, Jotai, Redux).
  • Effects and lifecycle — useEffect, useLayoutEffect, or any code that runs after mount.
  • Browser APIs — window, document, localStorage, IntersectionObserver, geolocation.
  • Event handlers — onClick, onChange, onSubmit, or any user interaction handler.
  • Third-party client libraries — any package that internally uses hooks or browser APIs.

If your component doesn't need any of the above, leave it as a Server Component. It will render on the server, send zero JavaScript to the browser, and have direct access to your backend — databases, file system, environment secrets, and APIs — without any network round-trip.

Understanding the "use client" Boundary

This is where most confusion happens. "use client" is not a per-component annotation — it's a module boundary. When you put "use client" at the top of a file, that file and every module it imports become part of the client bundle. It's a one-way door: once you cross it, everything downstream is client.

How the Component Tree Actually Works

Imagine your component tree as a waterfall. At the top is your root layout — a Server Component. It can render other Server Components freely. The moment it renders a component from a file marked with "use client", that's the boundary. Below that boundary, everything is client.

But here's the critical nuance that trips people up: a Client Component can still receive Server Components as children via the children prop. The Server Component is rendered on the server, serialized, and passed down. The Client Component doesn't re-render it — it just places it in the DOM.

Rule of thumb: "use client" pushes the boundary down. The children pattern lets you keep Server Components below a Client Component boundary.

The Pattern in Practice

Consider a page with a static header, an interactive tab bar, and a data-heavy content section. The wrong approach is to make the entire page a Client Component because the tab bar needs state. The right approach:

  1. Page layout (Server Component) — fetches data, renders the static header and content.
  2. TabBar (Client Component with "use client") — manages the active tab state.
  3. Content panels (Server Components) — passed as children to the TabBar.

This way, only the TabBar ships JavaScript to the browser. The header and content panels are pure HTML from the server. The tab bar receives pre-rendered content as children and just swaps which panel is visible.

Data Fetching: The Server Component Advantage

One of the biggest wins of Server Components is direct data access. In a Server Component, you can call your database, read files, hit internal APIs, or use secret environment variables — all without exposing anything to the client and without building an API route.

Fetching in Server Components

Server Components support async/await natively. You can make your component an async function and fetch data right in the body. The fetch happens on the server during rendering, and only the resulting HTML is sent to the client.

  • Use the native fetch API — Next.js extends it with caching and revalidation options.
  • Or call your database/ORM directly — Prisma, Drizzle, raw SQL, whatever your stack uses.
  • Access server-only secrets via process.env without prefixing NEXT_PUBLIC_.
  • No useState, no useEffect, no loading states — the data is ready before the HTML is sent.

Fetching in Client Components

Client Components cannot be async. They cannot use await at the top level. If a Client Component needs data, you have three options:

  1. Prop drilling from a parent Server Component — the server fetches, the client receives. This is the most common and most efficient pattern.
  2. Client-side fetching with useEffect or a library like SWR/React Query — for data that changes after the initial render (polling, real-time updates, user-triggered fetches).
  3. Server Actions — functions that run on the server but can be called from Client Components via form actions or direct invocation. Great for mutations and form submissions.

The golden rule: fetch data as high up the tree as possible in a Server Component, then pass it down to Client Components as props. This minimizes client-side JavaScript and eliminates loading spinners for initial data.

Composition Patterns That Work in Production

Pattern 1: Client Wrapper + Server Children

When you need interactivity around server-rendered content, wrap it. Create a small Client Component that handles the interaction (a toggle, a dropdown, a modal trigger) and pass the heavy content as children from a Server Component. The children are server-rendered — zero JS overhead.

Pattern 2: Client Islands in a Server Sea

Think of your page as a mostly-static document with small interactive islands. The page itself is a Server Component. Interactive pieces — a search bar, a like button, a theme toggle — are small Client Components imported where needed. Each island adds only its own JavaScript to the bundle.

Pattern 3: Lift State, Not the Boundary

When two parts of the page share state, the instinct is to make a common parent a Client Component. Instead, create a small Client Component that only manages the shared state (a context provider or a state container), and keep the actual UI rendering in Server Components passed as children.

Pattern 4: Server Actions for Mutations

Forms and mutations used to require API routes or client-side fetch calls. With Server Actions, you define an async function with "use server" and call it directly from a form action or a Client Component event handler. The function executes on the server, can access your database, and the result flows back seamlessly.

The 10 Most Common Mistakes (and How to Fix Them)

1. Adding "use client" to a page or layout

Pages and layouts should almost always be Server Components. They're the data-fetching layer. If your page needs interactivity, extract the interactive part into a separate Client Component and import it. Don't make the whole page client-side — you'll lose server-side data fetching, SEO benefits, and the ability to use async/await.

2. Importing a Server Component into a Client Component file

Once you cross the "use client" boundary, everything imported in that file is client. If you import a Server Component into a Client Component file, it becomes a Client Component — losing all its server benefits. Instead, pass it as children or a render prop from a Server Component parent.

3. Using useEffect for initial data fetching

In the App Router, initial data should come from Server Components — not from useEffect in a Client Component. useEffect runs after mount, causing a loading flash. Server Components deliver the data with the HTML. Reserve useEffect for truly client-side concerns: polling, subscriptions, or data that changes based on user interaction.

4. Putting "use client" too high in the tree

Every component below the "use client" boundary ships JavaScript to the browser. If you put the directive in a layout or a high-level wrapper, you push thousands of lines into the client bundle unnecessarily. Push the boundary as low as possible — to the leaf components that actually need interactivity.

5. Trying to use hooks in a Server Component

useState, useEffect, useContext, and all React hooks require a client runtime. If you use them in a Server Component, Next.js will throw an error. The fix isn't to add "use client" to the file — it's to ask whether you really need client state, or whether you can restructure to keep the component server-rendered.

6. Passing non-serializable props across the boundary

When a Server Component passes props to a Client Component, those props must be serializable — plain objects, arrays, strings, numbers, booleans. You cannot pass functions, class instances, Dates (use ISO strings), Maps, Sets, or Symbols. This is because the data crosses a network boundary from server to client.

7. Fetching the same data in multiple Server Components

It's actually fine to do this. Next.js automatically deduplicates fetch requests in Server Components during a single render pass. If three components fetch the same URL, only one network request is made. Don't add complexity (context providers, prop drilling through many layers) to avoid duplicate fetches — the framework handles it.

8. Confusing Server Components with SSR

Server Components and Server-Side Rendering are different concepts. SSR renders your entire React tree to HTML on the server, then hydrates it on the client — all components ship JavaScript. Server Components render only on the server and never hydrate — they send HTML, not JavaScript. A page can use both: Server Components for static parts, Client Components (which get SSR'd and then hydrated) for interactive parts.

9. Not using the server-only package

If you have utility code that must never run on the client (database queries, secret-key operations), import "server-only" at the top of that module. This makes Next.js throw a build error if the module is ever imported into a Client Component — catching the mistake at compile time instead of runtime.

10. Making everything a Client Component because it's familiar

The biggest anti-pattern is treating the App Router like the Pages Router — making every component a Client Component because that's what you're used to. You'll end up with a larger JavaScript bundle, slower page loads, more loading spinners, and none of the performance benefits the App Router was designed to deliver. Start server-first and only add "use client" when you hit a real need.

Performance Impact: Why This Matters for Real Users

The Server Component model isn't just an architecture preference — it has measurable performance implications. Every component you keep on the server is JavaScript that never ships to the browser. On a typical content-heavy page, this can mean 30–60% less JavaScript in the client bundle.

Less JavaScript means faster Time to Interactive (TTI), better First Input Delay (FID), and improved Interaction to Next Paint (INP) — three Core Web Vitals metrics that directly affect your Google search rankings. For e-commerce and content sites, this is the difference between page 1 and page 2 of search results.

From real production apps I've worked on, moving data-fetching components from Client Components (with useEffect) to Server Components eliminated loading spinners on initial paint, reduced Largest Contentful Paint (LCP) by 200–400ms, and cut the JavaScript bundle size by 35–50% on content pages.

Migrating From Pages Router: A Practical Checklist

If you're moving an existing Pages Router app to the App Router, here's the sequence that minimizes breakage:

  1. Start with layouts — move your _app.tsx and _document.tsx logic into app/layout.tsx as a Server Component.
  2. Move pages one at a time — convert each page to a Server Component, moving getServerSideProps/getStaticProps logic directly into the component body as async/await.
  3. Identify interactive pieces — any component that uses hooks or event handlers gets "use client" and becomes an imported Client Component.
  4. Push boundaries down — if a page has one interactive widget among ten static sections, only the widget should be a Client Component.
  5. Replace API routes for data reads — if you had API routes just to fetch data for your own frontend, you can now do it directly in Server Components.
  6. Adopt Server Actions for mutations — replace client-side fetch-to-API-route patterns with "use server" functions.

Quick Decision Cheatsheet

Keep as Server Component when:

  • The component fetches data (database, API, file system).
  • The component accesses backend resources or secrets.
  • The component renders static or read-only content.
  • The component has no user interaction, state, or effects.
  • The component is large and would add significant JavaScript to the bundle.

Mark as Client Component ("use client") when:

  • The component uses React hooks (useState, useEffect, useContext, useRef, etc.).
  • The component handles user events (onClick, onChange, onSubmit).
  • The component uses browser-only APIs (window, document, localStorage).
  • The component relies on third-party libraries that use hooks or browser APIs.
  • The component needs to re-render in response to client-side state changes.

Final Thoughts

The Server Component vs Client Component decision is not about preference — it's about capability. Server Components can do things Client Components can't (direct data access, zero JS overhead), and Client Components can do things Server Components can't (interactivity, state, browser APIs). The architecture works best when you use each for what it's good at.

The mental model is straightforward: default to Server Components, push the "use client" boundary as low as possible, fetch data on the server, and pass it down as props. Once this clicks, the App Router stops feeling like a constraint and starts feeling like a superpower.

If you're migrating from the Pages Router, the transition is incremental — move one page at a time, and you'll build intuition fast. The React and Next.js ecosystem is moving toward server-first rendering for good reasons: smaller bundles, faster pages, better SEO, and a simpler data-fetching story. The sooner you internalize this mental model, the better your apps will be.