Next.js Server Components vs Client Components
Web DevelopmentLesson 2 of 45 min read
What you'll learn
- Explain what runs on the server and what runs in the browser in the App Router
- Decide between a server and a client component for a given feature
- Compose a server component that renders a small client component with serializable props
- Recognise and fix the errors caused by mixing hooks, handlers and server-only code
On this page
A ferry operator wants a live departures board. Visitors should see the timetable instantly, and they should be able to filter it by terminal with a button. The timetable comes from a database; the filter button needs state. You do not need the whole page in the browser to do that. You need the database call on the server and a small interactive piece in the browser. In the App Router, that split is the default way of thinking, and "use client" is the line that draws the border.
Two kinds of components, one tree
In the App Router every component is a Server Component unless you opt out. Server Components run only on the server (at build time or per request). They can be async, can read databases and secrets directly, and send only their rendered result to the browser, so none of their code, or the libraries they import, is shipped as JavaScript. A Client Component is declared by putting the directive "use client" at the top of its file. It is rendered on the server for the first HTML, then hydrated in the browser, which is when state, effects and event handlers come alive.
| Capability | Server Component | Client Component |
|---|---|---|
async function and await in the body | Yes | No |
| Read a database, file or secret directly | Yes | No (it would reach the browser) |
useState, useEffect, other hooks | No | Yes |
onClick, onChange handlers | No | Yes |
Browser APIs like window, localStorage | No | Yes (in effects or handlers) |
| Adds to the browser JavaScript bundle | No | Yes |
| Can import the other kind | Can render client components | Can receive server components as children |
The departures board, built the island way
Start with the data layer. Marking it with the small server-only package (install it with your package manager) makes the build fail if a client component ever imports it, which protects database code and secrets.
import "server-only";
export type Terminal = "North" | "South";
export type Departure = {
id: string;
terminal: Terminal;
destination: string;
time: string;
};
const SCHEDULE: Departure[] = [
{ id: "d1", terminal: "North", destination: "Calder Island", time: "08:10" },
{ id: "d2", terminal: "South", destination: "Wren Quay", time: "08:25" },
{ id: "d3", terminal: "North", destination: "Wren Quay", time: "08:40" },
];
// In a real app this would be a database or API call.
export async function getDepartures(): Promise<Departure[]> {
return SCHEDULE;
}Because the page below is a Server Component, it can await the data directly, with no useEffect, no loading state for the first render and no API route. It then hands the rows to a client component as a prop.
import { getDepartures } from "@/lib/departures";
import DepartureBoard from "./DepartureBoard";
export default async function DeparturesPage() {
const departures = await getDepartures();
return (
<main>
<h1>Ferry departures</h1>
<p>Updated each time you load the page.</p>
<DepartureBoard departures={departures} />
</main>
);
}"use client";
import { useState } from "react";
import type { Departure, Terminal } from "@/lib/departures";
export default function DepartureBoard({ departures }: { departures: Departure[] }) {
const [terminal, setTerminal] = useState<Terminal | "All">("All");
const visible =
terminal === "All" ? departures : departures.filter((d) => d.terminal === terminal);
return (
<section>
<div role="group" aria-label="Filter by terminal">
{(["All", "North", "South"] as const).map((option) => (
<button
key={option}
type="button"
aria-pressed={terminal === option}
onClick={() => setTerminal(option)}
>
{option}
</button>
))}
</div>
<ul>
{visible.map((d) => (
<li key={d.id}>
{d.time} to {d.destination} ({d.terminal})
</li>
))}
</ul>
</section>
);
}The browser first receives complete HTML: the heading, the sentence, three buttons and all three departures as a list, so the page is readable before any JavaScript has loaded. After hydration, pressing "South" updates the list to a single line, "08:25 to Wren Quay (South)", without a network request, because the full list is already in the component's props. The import type from the server-only file is allowed in a client component because types are erased at compile time and no server code is bundled.
Rules for what can cross the border
Props passed from a Server Component to a Client Component are serialised and sent to the browser, so they must be serialisable: strings, numbers, booleans, plain objects and arrays, and a few other built-in types. Functions and class instances cannot cross. Also, a client component cannot import a server component, because the import would pull its code into the browser bundle. It can, however, receive a server component as children or another prop, which is how you wrap server content in a client-side provider.
// app/providers.tsx
"use client";
import { createContext, useContext, useState, type ReactNode } from "react";
type Density = "comfortable" | "compact";
const DensityContext = createContext<Density>("comfortable");
export function useDensity() {
return useContext(DensityContext);
}
export function Providers({ children }: { children: ReactNode }) {
const [density] = useState<Density>("comfortable");
return <DensityContext.Provider value={density}>{children}</DensityContext.Provider>;
}
// app/layout.tsx (still a Server Component)
import { Providers } from "./providers";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}The children passed into Providers are still rendered on the server. Only the provider itself is client code, so the pages inside it keep their server-side benefits. React context does not exist in Server Components, which is why every provider has to be a client component like this one.
Common mistakes
- Using a hook in a Server Component. Next.js reports an error along the lines of: you're importing a component that needs
useState, which only works in a Client Component, and asks you to add the"use client"directive to the file or a parent. - Passing a handler from a server file.
<Button onClick={() => ...} />whereButtonis a client component but the handler is created in a server file fails withEvent handlers cannot be passed to Client Component props. Create the handler inside a client component instead. - Marking a whole layout `"use client"`. Everything beneath it becomes client code, and bundle size grows without a visible error.
- Reading `window` or `localStorage` during render. On the server this throws
window is not defined. Read browser APIs insideuseEffector an event handler. - Different output on server and client. Rendering
Date.now()orMath.random()directly produces a hydration warning that the text content does not match the server-rendered HTML. Compute such values in an effect, or pass them from the server. - Expecting a secret to be hidden after importing it into a client file. Only environment variables prefixed with
NEXT_PUBLIC_reach the browser, but any value you hard-code or pass as a prop does. Useserver-onlyfor modules that must never ship.
How this shows up in real projects and interviews
"What is the difference between a server and a client component?" is now a standard question for React and Next.js roles, and the follow-up is usually "when would you choose each?". Strong answers describe the default (server), the reasons to opt in to the client (state, effects, browser APIs, event handlers), and the pattern of small interactive islands inside server pages. Team conventions often put a "use client" file next to each page for its interactive part. For the hooks you will use in those files, revisit React useState and React Events and Forms.
Try it yourself
Build a /movies page as a Server Component that reads a hard-coded array of six movies from lib/movies.ts (marked server-only). Add a client component that offers a text box to filter by title and a button to toggle sort order. Then deliberately introduce each error from the mistakes list, such as a hook in the page, and read the message Next.js prints.
Show a hint
Pass the movies as a prop. Use useMemo or just compute the visible list in the render. To trigger the hook error, add useState to page.tsx and look at the dev overlay.
Key takeaways
- In the App Router, components are Server Components by default; add
"use client"only where you need state, effects, handlers or browser APIs. - Server Components can be async and access data and secrets directly, and their code is not shipped to the browser.
- Props crossing from server to client must be serialisable; functions cannot cross, and a client component cannot import a server component.
- Keep the client boundary small by extracting interactive islands, and wrap providers around server
children.
Test yourself
A trick from this lesson's ride: 'use client' means 'also ships to the browser', not 'skips the server'. Four more tricks like it wait along the route, each followed by a question.
Frequently asked questions
What is the difference between server components and client components in Next.js?
Server Components run only on the server, can be async, can access data and secrets and add no JavaScript to the browser bundle. Client Components, marked with "use client", are also pre-rendered on the server but are hydrated in the browser, so they can use state, effects and event handlers.
Does use client mean the component only runs in the browser?
No. A client component is still rendered to HTML on the server for the first load and then hydrated in the browser. The directive means its code is included in the browser bundle and it can use browser-only features, but those must be used inside effects or handlers.
Can I use useState or useEffect in a Next.js page?
Only inside a Client Component. If a page needs state, either add "use client" to that file or, better, keep the page a Server Component and move the interactive part into a separate client component that the page renders.