Server Components as the default
How the Next.js App Router makes every component a React Server Component by default, running on the server and shipping zero JavaScript to the browser.
You need to show a list of invoices on a page. You know how to do this in plain React, and you know it’s more ceremony than it should be. The component can’t just have the data, so you reach for the familiar dance: a piece of state to hold the invoices, an effect to go fetch them after the component mounts, a loading flag to render something while you wait, and a re-render once the data finally lands.
'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} />;};Look at everything that snippet is paying for. There are two pieces of state. There’s an effect that runs only after the component has already rendered once, empty-handed. There’s a spinner the user watches while a second network round trip happens: the page loaded, and only then did the browser go ask for the data. And because that fetch runs in the browser, the /api/invoices endpoint has to be public, and any auth header you attach to it ships in code the user can read. Every line here exists to work around one constraint: the component runs in the browser, and the browser doesn’t have your data.
Now the same page in the App Router:
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 that needs the data just asks for it, awaits the answer, and renders. The reason it can do that is the whole subject of this lesson: this component runs on the server, during the request, so it can reach the database directly, and the browser only ever receives the finished result, never this code.
You’ve been writing files exactly like this since the previous chapter on the App Router, and every time we called them “Server Components by default” and quietly promised to explain what that meant later. This is that explanation. By the end of this lesson you’ll be able to look at any file under app/ and know on sight that it 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 in the browser
Section titled “A component that runs on the server, not in the browser”The rest of the chapter rests on one fact: every component under app/ is a React Server Component by default. There’s no directive at the top of the file, no special import, no opt-in. The page.tsx files you wrote in the previous chapter were Server Components, and nobody had to tell them to be.
To see why that matters, hold two kinds of component side by side.
The React you’ve written until now is JavaScript that ships to the browser and runs there. The user’s browser downloads your component’s code, runs it, and the component lives there. It can hold state, respond to clicks, and read the URL bar, because it’s present in the page the whole time the user is on it.
A Server Component is JavaScript that runs on the server, once, during the request, and never ships to the browser at all. It executes while Next.js is building 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. The exact format of what crosses the wire is a later lesson; for now, “the browser gets the output, not the code” is the whole idea.
That one difference, where the code runs, leads to everything else. It’s worth spelling out, because every capability in this lesson follows from it:
- It runs on the server, so it has access to everything a server has: the database, the filesystem, environment variables, internal services.
- Its code never reaches the browser, so it adds nothing to the bundle the user downloads.
- And on the flip side, it can’t do anything that needs a browser. No state that survives past the render, no click handlers, no
window. There’s nothing of it left in the browser to do those things.
In the previous chapter, the framing was “the framework fills in children; you just receive them.” Here’s the larger version of that same idea: the framework also decides where each component runs. You write the same JSX you always have, and Next.js places it on the server unless something explicitly tells it otherwise. Server is the default, and it’s automatic.
The bundle saving is the clearest payoff. Say your Server Component imports a markdown-to-HTML library, a syntax highlighter, or a date-formatting library, the kind of dependency that’s hundreds of kilobytes. In old-style React, every one of those bytes ships to the browser and the user waits for them to download. In a Server Component, that library runs on the server, does its job, and ships zero kilobytes to the user. They download the result, not the tool that produced it.
Keep this geography in your head for the whole chapter, because every later fact gets pinned to it. 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.
Awaiting data in the component body
Section titled “Awaiting data in the component body”Look again at the canonical page from the introduction, specifically at its first line:
export default async function InvoicesPage() {That async is the part that’s genuinely new. A Server Component can be an async function, and you can await at the top of its body. This is impossible in browser React, where a component can never be an async function, because React has to be able to call it and get JSX back synchronously, render after render. A Server Component runs exactly once and returns a value once, so it’s free to be async. React waits for the Promise, then renders with the result.
That unlocks fetching data right where you render it. The data read lives in the component body, next to the JSX that consumes it:
const invoices = await listInvoices(); // your data layerconst config = await fetch('https://api...'); // an HTTP callconst file = await readFile('./report.md'); // the filesystemThere’s no separate loader function sitting beside the component, and no special export the framework calls before rendering to hand you props. If you’ve seen older Next.js code, you may have met getServerSideProps, a function you exported and the framework called to fetch data before rendering the page. That’s the previous-generation pattern, and it’s gone. You don’t need it, because the component can now do the fetching itself.
Here’s the habit worth building 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 some parent and thread it down through props “to be tidy”; you co-locate the read with the render, so the data lives next to where it’s used.
One thing to name so it doesn’t surprise you later: when a page reads the same data on every request, you usually don’t want to hit the database every single time. Next.js can cache fetch results, and React gives you a cache() helper to deduplicate reads within a request. That’s real, and it’s the subject of its own chapter later. For now, read every await in this lesson as a direct, uncached fetch: the data is fetched fresh, in render, on the server.
Here’s the full canonical page, the shape you’ll write on nearly every route from here on. This version 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 makes this a component you can await inside, which is only possible 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. You already met this in the previous chapter: 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 the time we render, invoice is already here. There’s no loading state to handle, because there’s nothing to wait for: the data was ready before the JSX ran.
That async shape is worth drilling once so it becomes automatic. In the page below, two keywords are missing: the one that makes the function awaitable, and the one in front of 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} />;}What only a Server Component can do
Section titled “What only a Server Component can do”Running on the server isn’t just a location; it’s a set of powers the browser will never have. This is the half of the ledger that makes Server Components worth defaulting to. Each item here 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, use them, and ship none of them. 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();There’s a Next.js-specific rule worth stating precisely, because it’s the framework’s guardrail around exactly this. Only environment variables prefixed with NEXT_PUBLIC_ are exposed to the browser. Everything else is server-only by default, and if you reference an unprefixed variable in code that does run in the browser, it comes back as an empty string. The framework draws the line for you. But don’t lean on the prefix as the rule; 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 5Import heavy, server-only dependencies for free. The Stripe Node SDK, a markdown parser, a syntax highlighter, a library that renders emails to HTML: 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. Render trees as large and as dependency-heavy as you like; the user downloads the result, never the machinery.
Here’s why this isn’t an academic nicety. In a real SaaS, the distance between “the Stripe secret key lives on the server” and “the Stripe secret key shipped in the browser bundle” is the distance between a working product and a security incident someone finds in your public JavaScript. The default puts you on the safe side of that line automatically; you have to go out of your way to cross it.
Which is why the next point is the one to watch for.
What a Server Component can’t do
Section titled “What a Server Component can’t do”Every power has a matching limit, and the limits are where the next lesson comes from. A Server Component runs once and is gone: it produces its output and leaves nothing behind in the browser. So anything that needs a living component, something present in the page and reacting over time, is off the table. Here’s the list, each with the one-line reason.
- No state hooks (
useState,useReducer). State has to be held somewhere between renders, and there’s no living instance of this component in the browser to hold it. - No effects or lifecycle (
useEffect,useLayoutEffect, auseRefpointing at a DOM node). Nothing ever mounts in the browser, so there’s no “after render” for these to run in. - No event handlers (
onClick,onChange,onSubmit). Wiring up a handler requires JavaScript in the browser to attach it, and a Server Component ships none. - No browser globals (
window,document,localStorage,navigator). These objects simply don’t exist on a server; there’s no window when the code runs. - No Context, directly. Reading a React Context needs a provider, and providers are a client-side concern. Treat Context as something that lives on the browser side of the boundary.
Notice that all five collapse into one sentence: they’re all things that need a living component in the browser. A Server Component has no life after it returns its markup. The instant a feature has to respond to the user over time, whether that’s a click, a keystroke, or a value that changes, it needs the browser. And needing the browser is precisely what the next lesson, on Client Components, is about. The fix for every item on this list is to opt that piece into the client with a directive called "use client", which we’ll name now and unpack next.
That split, server powers on one side and browser needs on the other, is the whole mental model. Test it by sorting 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.
await fetch(...)process.env.STRIPE_SECRET_KEYuseStateonClick handlerlocalStorageuseEffect cleanupwindow.scrollYComposing Server and Client Components
Section titled “Composing Server and Client Components”Real pages aren’t all-server or all-client. A page is a Server Component, and somewhere inside it there’s a button that needs a click handler or a date picker that needs state. So Server and Client Components have to nest inside each other, and how they nest is the thing beginners get wrong most often. There are exactly three moves to know: two are legal and one isn’t, and the illegal one is worth understanding rather than memorizing.
Move one: a Server Component renders a Client Component by importing it. This is the common case, and it just works. Your Server Component page imports an interactive leaf such as a <BuyButton /> or a <DatePicker />, renders it, and passes it props. The page stays on the server, and the button is the small piece that goes to the browser. This is the everyday shape of an App Router page: a wide server tree with a few interactive leaves hanging off it.
Move two is illegal: a Client Component cannot import a Server Component. To see why, you need one fact about the directive that marks the client boundary, "use client": a file marked "use client" pulls everything it imports into the browser bundle along with it. The full mechanism is a later lesson; the headline is enough here. So if a Client Component were allowed to import a Server Component, that server code, with its database access, its secret-reading, and its 200 KB markdown library, would be dragged into the browser right along with it. That defeats the entire model, so the framework forbids it, and it does so loudly: importing a Server Component into a Client Component is 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 or a collapsible panel around a report? That’s 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 is rendered 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, and the server content is its filling. The shell knows it has some children to render; it never knows what they were made of.
You’ve already written this once. In the previous chapter, the URL-backed modal was a <Modal> Client Component that wrapped a server-rendered <PhotoDetail> passed in as {children}. That was this exact pattern, before you had a name for it. Here’s the name and the rule behind it.
The one-liner to keep: wrap, don’t import. When a Client Component needs server-rendered content inside it, the 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:
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.
'use client';
import { InvoiceDetail } from './invoice-detail';
export const Sidebar = () => { const [open, setOpen] = useState(false); return open ? <InvoiceDetail /> : null;};Illegal. A "use client" file drags everything it imports into the browser bundle, so importing a Server Component would haul its database access and secrets to the client. The framework errors at build time.
'use client';
export const Modal = ({ children }: { children: React.ReactNode }) => { const [open, setOpen] = useState(true); return open ? <dialog open>{children}</dialog> : null;};import { Modal } from '@/app/_components/modal';import { InvoiceDetail } from './invoice-detail';
export default async function InvoiceModalPage() { return ( <Modal> <InvoiceDetail /> </Modal> );}The fix: wrap, don’t import. The Server Component is passed down as children, and the Client shell renders it without ever importing it. This <Modal> is a stripped-down shell, a bare native <dialog> rather than the real shadcn <Dialog> you built in the previous chapter, so that the only thing in view here is the {children} prop carrying the server content across.
There’s one more misconception this composition hides, and it’s worth pulling into the open because it trips up nearly everyone. When a Server Component renders a Client Component, you might assume the client one skips the server entirely and only runs in the browser. It doesn’t. Every component runs on the server first, including the Client ones. The server renders the whole tree, Client Components included, to produce the initial HTML the user sees immediately. The Client Component then “wakes up” in the browser afterward 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 below 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 from left to right and watch each node change state.
Every component runs on the server first, even the client one. BuyButton renders to HTML right here; "use client" does not mean “skip the server”.
Only now does BuyButton become interactive. InvoiceList shipped zero client JS, because its work was already finished on the server.
What actually crosses that wire (which props make it to the browser, which ones throw, and which ones leak a secret you didn’t mean to send) is a lesson of its own, two ahead. For now you’ve got the geography: server tree, client leaves, and the one rule for nesting them.
One last contrast to hold onto, because it shapes how much the framework protects you. The illegal move, a Client Component importing a Server Component, fails loudly, at build time, with an error. The framework catches it for you, 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 of these and not the other, so the one it doesn’t guard is the one you have to watch yourself.
Recap and what’s next
Section titled “Recap and what’s next”The inversion is the whole lesson: in the App Router, a component runs on the server by default, once per request, and ships no JavaScript to the browser. You opt into the browser; you don’t opt out of it.
- The shape:
export default async function Page(), where youawaityour data in the body, with no effect, no loader, and no spinner. - The two ledgers: the server gives you data, secrets, and zero bundle cost; the client gives you state, events, and browser APIs. Every line of code lands on one side.
- The composition rule: a Server Component renders a Client Component by importing it; a Client Component renders a Server Component only by receiving it as
children, never the reverse import. Wrap, don’t import.
Everything on the can’t list (state, clicks, browser APIs) is what Client Components unlock. But opting into the browser isn’t free; it costs JavaScript, and an experienced engineer spends that cost deliberately, at the smallest leaf that needs it. That trade-off, and the "use client" directive that draws the line, is next.
If you’d like the whole server/client picture narrated end to end before moving on, this video walks the same arc this lesson did: retiring the fetch dance, keeping secrets and heavy libraries on the server, and nesting interactive leaves inside a server tree.
External resources
Section titled “External resources”The canonical reference this lesson tracks, including the RSC Payload and composition patterns.
React's own reference for the async component and the server/client split.
Josh W. Comeau's interactive walkthrough, with diagrams that visualize where each render happens.
Dan Abramov reframes the directive as a network boundary inside one program spanning two environments.