Skip to content
Chapter 30Lesson 1

Server Components as the default

Why every component in the Next.js App Router is a React Server Component by default, running on the server and shipping no JavaScript to the browser.

You need to show a list of invoices on a page. In plain React the component can’t just have the data, so you reach for the usual pieces: state to hold the invoices, an effect to fetch them after the component mounts, a loading flag to render something while you wait, and a re-render once the data arrives.

invoices-page.tsx (the old way)
'use client';
export const InvoicesPage = () => {
const [invoices, setInvoices] = useState<Invoice[]>([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetch('/api/invoices')
.then((res) => res.json())
.then((data) => {
setInvoices(data);
setIsLoading(false);
});
}, []);
if (isLoading) return <Spinner />;
return <InvoiceTable invoices={invoices} />;
};

Every line here works around one constraint: the component runs in the browser, and the browser doesn’t have your data. So the page loads empty, then makes a second network round trip for the data while the user watches a spinner. And because that fetch runs in the browser, the /api/invoices endpoint must be public, and any auth header you attach ships in code the user can read.

Now the same page in the App Router:

src/app/invoices/page.tsx
export default async function InvoicesPage() {
const invoices = await listInvoices(); // data layer: Unit 5
return <InvoiceTable invoices={invoices} />;
}

No state, no effect, no loading flag, no exposed endpoint. The component asks for the data, awaits it, and renders. It can do this because it runs on the server, during the request: it reaches the database directly, and the browser only ever receives the finished result, never this code.

You’ve been writing files like this since the previous chapter on the App Router, where we called them “Server Components by default.” By the end of this lesson you’ll know on sight that any file under app/ runs only on the server, write the data-fetching page above without thinking about it, and predict, for any line of code, whether it can run on the server or needs the browser.

A component that runs on the server, not the browser

Section titled “A component that runs on the server, not the browser”

The rest of the chapter rests on one fact: every component under app/ is a React Server Component by default. No directive, no special import, no opt-in. The page.tsx files from the previous chapter were already Server Components.

The React you’ve written until now is JavaScript that ships to the browser and runs there. The browser downloads your component’s code and runs it, so the component lives in the page the whole time the user is on it. That’s why it can hold state, respond to clicks, and read the URL bar.

A Server Component runs on the server, once, during the request, and never ships to the browser at all. It executes while Next.js builds the response, produces some markup, and then it’s done. The browser receives that markup, the output of the component, and never sees the component’s code. (What exactly crosses the wire is a later lesson; for now, “the browser gets the output, not the code” is the whole idea.)

Everything in this lesson follows from that one difference, where the code runs:

  • It runs on the server, so it can reach everything a server can: the database, the filesystem, environment variables, internal services.
  • Its code never reaches the browser, so it adds nothing to the bundle the user downloads, not even a heavy dependency it imports.
  • It can’t do anything that needs a browser: no state that survives the render, no click handlers, no window. Nothing of it is left in the browser to do those things.

This extends the previous chapter’s idea that the framework fills in children for you. The framework also decides where each component runs: you write the same JSX, and Next.js places it on the server unless something tells it otherwise. Server is the default, and it’s automatic.

Keep this geography in mind for the rest of the chapter. There’s a server on one side and a browser on the other, with a boundary between them. Everything a Server Component can do lives on the left; everything it can’t do lives on the right.

Server
Server Component — runs here, once, per request
await db process.env fs ships 0 KB JS
Browser
receives output only
The boundary every Next.js app is built around. A Server Component lives on the left; the browser, on the right, only ever receives what it produced.

Look again at the first line of the canonical page from the introduction:

export default async function InvoicesPage() {

That async is what’s new. A Server Component can be an async function, and you can await at the top of its body. Browser React forbids this: React has to call a component and get JSX back synchronously, render after render. A Server Component runs once and returns once, so it’s free to be async. React waits for the Promise, then renders with the result.

That lets you fetch data right where you render it, in the component body next to the JSX that consumes it:

const invoices = await listInvoices(); // your data layer
const config = await fetch('https://api...'); // an HTTP call
const file = await readFile('./report.md'); // the filesystem

There’s no loader function beside the component, and no special export the framework calls before rendering. Older Next.js had getServerSideProps, an export the framework called to fetch data before rendering; that’s gone, because the component now fetches for itself.

Build this habit now: fetch at the component that owns the data. The component that renders the invoice table is the one that reads the invoices. You don’t hoist the read up to a parent and thread it down through props; you co-locate the read with the render.

When a page reads the same data on every request, you won’t always want to hit the database each time: Next.js can cache fetch results and React’s cache() deduplicates reads within a request, the subject of a later chapter. For now, read every await here as a direct, uncached fetch.

Here’s the full canonical page, the shape you’ll write on nearly every route. It also reads the URL: recall from the previous chapter that a page receives its params as a Promise you await, typed with the generated PageProps.

export default async function InvoicePage({
params,
}: PageProps<'/invoices/[id]'>) {
const { id } = await params;
const invoice = await getInvoice(id); // data layer: Unit 5
return (
<article>
<h1>Invoice {invoice.id}</h1>
<p>Total: {invoice.total}</p>
<p>Status: {invoice.status}</p>
</article>
);
}

async lets you await inside the component, possible only because it runs once, on the server, not in a live browser.

export default async function InvoicePage({
params,
}: PageProps<'/invoices/[id]'>) {
const { id } = await params;
const invoice = await getInvoice(id); // data layer: Unit 5
return (
<article>
<h1>Invoice {invoice.id}</h1>
<p>Total: {invoice.total}</p>
<p>Status: {invoice.status}</p>
</article>
);
}

The URL’s params arrive as a Promise, so you await them. PageProps<'/invoices/[id]'> types it for you, so you never hand-write the Promise.

export default async function InvoicePage({
params,
}: PageProps<'/invoices/[id]'>) {
const { id } = await params;
const invoice = await getInvoice(id); // data layer: Unit 5
return (
<article>
<h1>Invoice {invoice.id}</h1>
<p>Total: {invoice.total}</p>
<p>Status: {invoice.status}</p>
</article>
);
}

The data is fetched right here, in render, on the server. No effect, no loader, no second round trip: getInvoice reaches the database directly.

export default async function InvoicePage({
params,
}: PageProps<'/invoices/[id]'>) {
const { id } = await params;
const invoice = await getInvoice(id); // data layer: Unit 5
return (
<article>
<h1>Invoice {invoice.id}</h1>
<p>Total: {invoice.total}</p>
<p>Status: {invoice.status}</p>
</article>
);
}

By render time, invoice is already here. There’s no loading state, because there’s nothing to wait for.

1 / 1

Drill the shape once so it becomes automatic. Two keywords are missing below: the one that makes the function awaitable, and the one before the data call. Fill them in.

Complete the canonical Server Component page. Pick the right option from each dropdown, then press Check.

export default ___ function InvoicesPage() {
const invoices = ___ listInvoices();
return <InvoiceTable invoices={invoices} />;
}

Running on the server is a set of powers the browser will never have. Each item below is something the browser literally cannot do, and the default hands it to you for free.

Read secrets without leaking them. A Server Component can read process.env.STRIPE_SECRET_KEY, a database URL, or an internal API token and use it, all without shipping it. The code that touches the secret never reaches the browser, so the secret doesn’t either.

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
const charges = await stripe.charges.list();

Next.js draws this line for you: only environment variables prefixed with NEXT_PUBLIC_ are exposed to the browser. Everything else is server-only, and an unprefixed variable referenced in browser code comes back as an empty string. Don’t lean on the prefix, though; the durable rule is simpler: secrets live on the server.

Query the database directly. For your app’s own reads, there’s no API layer between the page and the data. The component calls into your data layer and awaits a row: no route handler to write, no endpoint to secure, no JSON to parse. The page is the consumer of the query.

const invoices = await db.select().from(invoicesTable); // data layer: Unit 5

Import heavy, server-only dependencies for free. The Stripe Node SDK, a markdown parser, a syntax highlighter, an email renderer: import them into a Server Component and they run server-side, contributing zero bytes to the browser bundle. A 200 KB markdown-to-HTML pipeline costs the user nothing, because only its output crosses the boundary. The user downloads the result, never the machinery.

This isn’t an academic nicety. In a real app, the gap between “the Stripe secret key lives on the server” and “the Stripe secret key shipped in the browser bundle” is the gap between a working product and a security incident in your public JavaScript. The default keeps you on the safe side of that line; you have to go out of your way to cross it.

A Server Component runs once and is gone: it returns its markup and leaves nothing behind in the browser. So anything that needs a living component, one that stays on the page and reacts over time, is impossible.

  • No state hooks (useState, useReducer). There’s no living instance in the browser to hold state between renders.
  • No effects or lifecycle (useEffect, useLayoutEffect, a useRef on a DOM node). Nothing mounts in the browser, so there’s no “after render” to run in.
  • No event handlers (onClick, onChange, onSubmit). Attaching a handler needs JavaScript in the browser, and a Server Component ships none.
  • No browser globals (window, document, localStorage, navigator). These objects don’t exist on a server.
  • No Context, directly. Reading a Context needs a provider, and providers live on the client.

All five reduce to the same thing: they need a living component in the browser, and a Server Component has no life after it returns its markup. The moment a feature has to respond to the user over time, whether a click, a keystroke, or a changing value, it needs the client. The fix is to opt that piece in with a directive called "use client", which the next lesson unpacks.

Sort each capability below into where it can run.

Sort each capability into where it can run. Drag each item into the bucket it belongs to, then press Check.

Server Component Runs on the server, ships no JS
Needs a Client Component Needs a living component in the browser
await fetch(...)
Reading process.env.STRIPE_SECRET_KEY
Querying the database directly
Rendering markdown to HTML
Importing a heavy Node SDK
useState
An onClick handler
Reading localStorage
A useEffect cleanup
Reading window.scrollY

Real pages aren’t all-server or all-client. A page is a Server Component, but somewhere inside it a button needs a click handler or a date picker needs state. Server and Client Components have to nest, and how they nest is where beginners go wrong. There are three moves: two legal, one illegal.

Move one: a Server Component imports and renders a Client Component. This is the common case, and it just works. A Server Component page imports an interactive leaf like <BuyButton />, renders it, and passes props. The page stays on the server; the button is the small piece that ships to the browser. That is the everyday shape of an App Router page: a wide server tree with a few interactive leaves.

Move two is illegal: a Client Component cannot import a Server Component. One fact about "use client" explains why: a file marked "use client" pulls everything it imports into the browser bundle. (The full mechanism is a later lesson.) So importing a Server Component into a Client Component would drag that server code, with its database access, its secrets, and its heavy libraries, into the browser. That defeats the model, so the framework forbids it as a build-time error: the build fails with a clear message before anything ships.

So how does interactive UI ever wrap server-rendered content, like a modal around an invoice? That is the third move.

Move three: a Client Component receives a Server Component as children. A Client Component can’t import a Server Component, but it can accept one through children or any prop slot. The Server Component renders on the server ahead of time into finished output, and that output is handed to the Client Component, which slots it in without ever seeing the source. The Client Component is an interactive shell; the server content is its filling.

You’ve written this already. In the previous chapter, the URL-backed modal was a <Modal> Client Component wrapping a server-rendered <PhotoDetail> passed in as {children}. Here is the rule behind it: wrap, don’t import. When a Client Component needs server-rendered content, its parent Server Component passes that content down as children; the Client Component never reaches across the boundary to import it.

The three moves, side by side:

src/app/invoices/[id]/page.tsx
import { BuyButton } from './buy-button';
export default async function InvoicePage({ params }: PageProps<'/invoices/[id]'>) {
const { id } = await params;
const invoice = await getInvoice(id);
return (
<article>
<h1>Invoice {invoice.id}</h1>
<BuyButton invoiceId={invoice.id} />
</article>
);
}

The common case. A Server Component imports a Client Component, passes props, and renders it. The page stays on the server; only BuyButton ships to the browser.

One misconception hides inside move one. When a Server Component renders a Client Component, you might assume the Client one skips the server and runs only in the browser. It doesn’t. Every component runs on the server first, Client ones included. The server renders the whole tree, Client Components and all, into the initial HTML the user sees immediately. The Client Component then wakes up in the browser to become interactive. "use client" doesn’t mean “don’t run on the server”; it means “also run in the browser, and become interactive there.”

Scrub through the trace to watch this happen. The tree is a server InvoicePage holding a server InvoiceList (which reads invoices from the database) and a client BuyButton. Drag the scrubber left to right and watch each node change state.

Where does each component run?

Which props actually cross that wire, and which ones leak a secret you didn’t mean to send, is a lesson of its own three ahead. For now you have the geography: server tree, client leaves, and the one rule for nesting them.

Hold onto one contrast, because it shapes how much the framework protects you. The illegal move, a Client Component importing a Server Component, fails loudly at build time, so you can’t ship it by accident. The secrets-in-props leak from the previous section does not fail: it builds fine, runs fine, and silently puts your secret in the browser. The framework guards one and not the other, so the unguarded one is the one you have to watch yourself.

Client Components unlock everything on the can’t list: state, clicks, and browser APIs. But the browser isn’t free; it costs JavaScript, so spend it deliberately, at the smallest leaf that needs it. The "use client" directive that draws that line is next.

For the whole server/client picture narrated end to end, this video walks the same arc: retiring the fetch dance, keeping secrets and heavy libraries on the server, and nesting interactive leaves inside a server tree.