Skip to the chapter
←ReactGGroundwork⚛React
React/Advanced/A4
37 / 57 written chapters
/

The cover
  1. B1Setup & JSX
  2. B2Thinking in React
  3. B3Components
  4. B4Props
  5. B5useState
  6. B6Events & conditionals
  7. B7Lists & keys
  8. B8Forms
  9. B9useEffect
  10. B10Lifting state & styling
  11. B11Guided project
  12. ★Cheat page
  1. I1useRef
  2. I2Context
  3. I3useReducer
  4. I4Custom hooks
  5. I5Effects in depth
  6. I6Routing
  7. I7State architecture
  8. I8Memoisation
  9. I9Boundaries & portals
  10. I10Data fetching
  11. I11Forms at scale
  12. I12State libraries
  13. I13Auth
  14. I14TanStack Query
  15. I15Testing
  16. I16TypeScript
  17. I17Animation
  18. I18E2E testing
  19. I19i18n
  20. I20File uploads
  21. I21Real-time UI
  22. I22shadcn/ui
  23. ★Cheat page
  1. A1Fiber
  2. A2Concurrent
  3. A3Suspense
  4. A4Server Components
  5. A5Server Actions
  6. A6use()
  7. A7Rendering strategies
  8. A8Effect timing
  9. A9External stores
  10. A10Refs in depth
  11. A11Component patterns
  12. A12Performance
  13. A13Accessibility
  14. A14Design systems
  15. A15Advanced TypeScript
  16. A16Architecture at scale
  17. A17Custom renderers
  18. A18Migrating & upgrading
  19. A19Production debugging
  20. A20Review & judgement
  21. A21State machines
  22. A22Feature flags
  23. A23Interview bank
  24. ★Cheat page

⚛A4 · Part 3 · Advanced

Server Components

Components that run once, on the server, and ship no JavaScript at all.

7 min read13 sections2 exercises
37 of 57

In this chapter

  • What they are
  • Not server-side rendering
  • The boundary
  • What crosses the boundary must serialise
  • Sharing data with the client tree through Context

and 8 more

about 7 min
13On this page
  1. What they are
  2. Not server-side rendering
  3. The boundary
  4. What crosses the boundary must serialise
  5. Sharing data with the client tree through Context
  6. What actually changes
  7. The rules, compressed
  8. What actually travels over the wire
  9. Request deduplication and cache
  10. Errors and loading, per segment
  11. Where it goes wrong
  12. The server boundary is an attack surface: React2Shell
  13. The mental model

What they are

A Server Component runs on the server, during the render, and its code is never sent to the browser. It can await a database query directly. It cannot use state, effects, or event handlers — because there is no browser to run them in and it never runs again.

// no "use client" — this is a Server Component by default in the App Router
export default async function Page({ params }) {
  const post = await db.post.findUnique({ where: { id: params.id } });
  return <article>{post.body}</article>;
}

No useEffect, no loading state, no API route in between, and the db client is not in the bundle.

Not server-side rendering

SSRServer Components
Runs on the serveryesyes
Runs again in the browseryes — hydrationno
Code shipped to the clientall of itnone of it
OutputHTMLa serialised element tree (RSC payload)

SSR renders your components on the server to produce HTML fast, then ships the same components down to run again. Server Components run only on the server; the client receives a description of the result, not the code that produced it.

The boundary

// app/page.tsx — server
import LikeButton from "./LikeButton";

export default async function Page() {
  const post = await getPost();
  return (
    <article>
      <h1>{post.title}</h1>
      <LikeButton postId={post.id} />      // island of interactivity
    </article>
  );
}

// LikeButton.tsx
"use client";
export default function LikeButton({ postId }) {
  const [liked, setLiked] = useState(false);
  return <button onClick={() => setLiked(!liked)}>...</button>;
}

"use client" marks the boundary. Everything that file imports is a client component too, transitively — so the directive is a doorway, not a per-component label. Put it as low in the tree as possible.

A client component can render a server one — as children
<ClientTabs>
  <ServerChart />      // ✓ passed as an element, already rendered
</ClientTabs>

A client component cannot import a server component — the server code would follow it into the bundle. It can receive one as children, because by then it is a rendered payload rather than code. This is the single most useful pattern for keeping an interactive shell around static content.

Server: runs once, never ships Page ProductList db.query(...) Client island "use client" AddToCart props must serialise
Everything above the line stays on the server. Only what crosses it, as serialisable props, and the client code itself, reaches the browser.

What crosses the boundary must serialise

<Client date={new Date()} />         // ✓ Dates, Maps, Sets are supported
<Client onSave={() => save()} />    // ✗ functions cannot cross
<Client user={classInstance} />     // ✗ class instances lose their prototype

Props are serialised into the RSC payload. Functions are the one that catches people — the exception being Server Actions, which are passed as a reference the client can call.

Sharing data with the client tree through Context

A Server Component cannot create Context — createContext needs the client. It can render one, as long as the Context is defined in a "use client" module.

// user-context.js
"use client";
import { createContext } from "react";
export const UserContext = createContext(null);

// layout.js — a Server Component, React 19.3 and later
import { UserContext } from "./user-context";

export default async function Layout({ children }) {
  const user = await getCurrentUser();
  return <UserContext value={user}>{children}</UserContext>;
}

Before React 19.3 the client module also had to export a wrapper — usually called UserProvider — whose only job was to take a prop and hand it straight to the Context. That file is gone now. It matters most for Contexts that exist only so server-fetched data, such as the current user or a feature-flag set, reaches the interactive components below.

The value still has to serialise, like any prop crossing the boundary. Pass the user object, not a class instance or a function.

What actually changes

  • Data fetching moves next to the data. No API layer built solely so the browser can reach the database.
  • Waterfalls collapse. Sibling Server Components fetch in parallel during one server render, instead of each discovering its needs after its parent painted.
  • Bundles shrink. A markdown renderer, a syntax highlighter, a date library used only for formatting — none of them ship.
  • Secrets stay put. API keys and database URLs are used in code that never reaches a browser.

The rules, compressed

ServerClient
async/await in the componentyesno
Database, filesystem, secretsyesno
State, effects, handlersnoyes
Browser APIsnoyes
Ships JavaScriptnoneyes

What actually travels over the wire

0:["$","article",null,{"children":[["$","h1",null,{"children":"Title"}],
   ["$L1",null,{"postId":7}]]}]
1:I["./LikeButton.js",["chunk-a.js"],"default"]

The RSC payload is a streamed, line-oriented format — not HTML and not JSON in the usual sense. Rendered server output is inline; $L1 is a placeholder for a client component, and the I line tells the browser which chunk to load for it.

Two consequences worth knowing. It streams, so the client can start rendering the top of the page before the bottom has been produced. And it describes elements, not HTML — which is why a client navigation can patch a subtree without discarding the state of the components around it. A full HTML response could not do that.

Request deduplication and cache

const getUser = cache(async (id) => db.user.find(id));

// three components calling getUser(7) in one render → one query

Because Server Components fetch where they are used rather than at the top, the same data is often requested several times in one render. React dedupes identical fetch calls automatically within a request, and cache() extends that to any function.

React 19.2 adds cacheSignal, which gives you an AbortSignal tied to the cache entry's lifetime — so work can be cancelled when the request that needed it goes away.

Errors and loading, per segment

app/dashboard/
  page.tsx
  loading.tsx      // a Suspense fallback for this segment
  error.tsx        // a client error boundary for this segment

Frameworks turn file conventions into the boundaries from Suspense and error boundaries. Worth knowing it is the same mechanism — error.tsx must be a client component, because error boundaries are still classes and still need to run in the browser.

Where it goes wrong

  • "use client" at the top of a layout. Everything below becomes client. The directive belongs on the leaf that needs it.
  • A client component importing a server utility that pulls in the database client — the build will tell you, loudly.
  • Fetching the same thing in three components. React dedupes identical requests within one render pass, but only if the calls are actually identical.
  • Expecting useState to work in a file with no directive. The error message is clear once you have seen it once.

The server boundary is an attack surface: React2Shell

On 3 December 2025 the React team disclosed CVE-2025-55182, rated CVSS 10.0 and widely called React2Shell. An unauthenticated attacker could send a crafted HTTP request to any Server Function endpoint and, when React deserialised it, run code on the server. Google's threat intelligence group saw widespread exploitation almost immediately, with cryptominers deployed from 5 December — two days after disclosure.

Follow-up disclosures on 11 December 2025 and in January 2026 added more, and one original fix turned out to be incomplete:

CVESeverityWhat it allowed
CVE-2025-55182Critical, 10.0Remote code execution through a Server Function request
CVE-2025-55184High, 7.5Denial of service — an infinite loop that hangs the server process
CVE-2025-67779High, 7.5Denial of service
CVE-2026-23864High, 7.5Denial of service — crashes, out-of-memory, runaway CPU
CVE-2025-55183Medium, 5.3Source code exposure from a Server Function that stringifies its arguments

The flaws were in react-server-dom-webpack, react-server-dom-parcel and react-server-dom-turbopack — the packages every RSC framework sits on, including Next.js, React Router, Waku, Parcel and the Vite RSC plugin. Safe versions are 19.0.4, 19.1.5 and 19.2.4 or later. Anyone who stopped at 19.0.3, 19.1.4 or 19.2.3 still had the incomplete DoS fix.

What to take from it
  • An app with no server is not affected. A pure client-side React app never deserialises a Server Function request. The risk arrived with the server boundary.
  • Every Server Function is a public endpoint. The attack needed no login and no knowledge of your code. Validate and authorise inside each one, as you would a REST handler.
  • Upgrade the framework, not only React. Frameworks bundle their own copy of the RSC runtime. Follow the framework's advisory, and do not rely on a hosting provider's firewall rule instead of patching.
  • Keep secrets out of source. CVE-2025-55183 leaked hardcoded strings, not process.env values. A key that lives only in an environment variable was not exposed by it.
  • Version hygiene is a skill. Knowing which version you run, reading advisories, and patching within days is part of shipping Server Components.

The mental model

Think of the page as mostly static content with islands of interactivity. The static parts are computed once on the server and cost nothing at runtime; the islands are small, and they are the only JavaScript the user downloads. That inverts the default of the last decade, where everything shipped and hydrated whether it needed to or not.

Interview answer, one sentence

"Server Components run only on the server and ship no JavaScript — unlike SSR, which runs the same components again in the browser. They can await data directly, which removes the API layer and the render-fetch waterfall, and 'use client' marks a doorway rather than a single component, so it belongs as low in the tree as possible."

02Practice this chapter

Opens in the editor: write it, run it, and check it against real tests.

  • 8 testsWhat actually crosses the boundaryadvanced
  • 8 testsImplement cache(), request deduplicationadvanced
All 54 for this level →
A4

Finished reading?

0 of 57 chapters read in this series

→Next stop

  1. A3Previous · A3Suspense & code splitting
  2. A4you are here
  3. Next · A5Server ActionsA function that runs on the server, called from the client, with no API route in between.6 min read

01On this page

13 sections · 0%

  1. 01What they are
  2. 02Not server-side rendering
  3. 03The boundary
  4. 04What crosses the boundary must serialise
  5. 05Sharing data with the client tree through Context
  6. 06What actually changes
  7. 07The rules, compressed
  8. 08What actually travels over the wire
  9. 09Request deduplication and cache
  10. 10Errors and loading, per segment
  11. 11Where it goes wrong
  12. 12The server boundary is an attack surface: React2Shell
  13. 13The mental model

[] chapters · t top