Server Components

Render components ahead of time on a server or at build time, outside the client bundle.

Signature
async function Page({ id }) {
  const note = await db.notes.get(id);
  return <div>{note}</div>;
}
  • Added in React 19.0

Parameters

ParameterTypeDescription
$propsoptionalobjectProps passed by the parent. Props crossing into a Client Component must be serializable; props passed between Server Components are unrestricted.

Return value

ReactNode (async Server Components return a Promise of one)The rendered output, serialized into the RSC payload and streamed to the client. The component's own code is never sent to the browser.

Errors

Rendering throws if a Server Component calls a client-only API such as useState, useEffect or an event handler, or if a non-serializable value is passed as a prop to a Client Component.

Changelog

  • PHP 19.0Server Components became stable and will not break between minor versions.
  • PHP 19.0The bundler-facing APIs used to implement Server Components do not follow semver and may break between 19.x minors; pin an exact React version in a framework or bundler.

A Server Component runs in an environment that is separate from both your client bundle and your SSR server — either once at build time on CI, or per request on a web server. Its code is never shipped to the browser; only its rendered output is.

When to reach for it

Reach for a Server Component whenever a component's job is getting data and shaping markup: reading a database or filesystem, calling an internal service, or rendering large content such as Markdown with a heavy parser. Because the component runs where the data lives, you skip the API route and the client-side fetch waterfall entirely, and the parser never enters the bundle.

What trips people up

There is no directive for Server Components. Outside a 'use client' subtree, components are Server Components by default; 'use server' marks Server Functions, not components. Anything interactive — state, effects, onClick, browser APIs — must live in a Client Component. Push the 'use client' boundary as far down as you can and pass server-rendered JSX through children, so an interactive wrapper does not drag its whole subtree onto the client.

To stream slow data, hand an unawaited promise to a Client Component and unwrap it there with use inside a <Suspense> boundary rather than awaiting it in the parent.

Example

Example · jsx
// database.js — server-only data access
const NOTES = {
  welcome: { title: 'Welcome', body: 'Server Components render before bundling.' },
};
const db = {
  notes: { get: async (id) => NOTES[id] },
};

// Expandable.jsx — a Client Component in its own file
// 'use client';
// import { useState } from 'react';
// export default function Expandable({ children }) {
//   const [expanded, setExpanded] = useState(false);
//   return (
//     <div>
//       <button onClick={() => setExpanded(!expanded)}>Toggle</button>
//       {expanded && children}
//     </div>
//   );
// }
import Expandable from './Expandable';

// NotePage is a Server Component: no directive, and it may await in render.
export default async function NotePage() {
  const note = await db.notes.get('welcome');
  return (
    <article>
      <h1>{note.title}</h1>
      <Expandable>
        <p>{note.body}</p>
      </Expandable>
    </article>
  );
}

When to use it

  • Query the database directly in a page component instead of building an API route for it.
  • Render Markdown or syntax-highlighted code at build time so the parser never ships to the browser.
  • Read a secret-bearing config or CMS token on the server and pass only the derived display values down.

Frequently asked questions

Why can't I use useState in a Server Component?

Because a Server Component runs once on the server and its code is never sent to the browser, so there is no client runtime to hold state or re-render it. Move the interactive part into a file with 'use client' at the top and pass the server-fetched data to it as props.

Do I need 'use server' to make a Server Component?

No — there is no directive for Server Components. Any component outside a 'use client' subtree is already a Server Component; 'use server' marks async functions the client is allowed to call over the network.

Can a Server Component import and render a Client Component?

Yes, and that is the intended direction: a Server Component may import a 'use client' module and render it, passing serializable props or JSX children. The reverse does not work — a Client Component cannot import a Server Component, though it can receive one as children.

More examples

Streaming slow data with Suspense and use

The note renders and flushes to the browser immediately, with "Loading comments…" in place of the list. When the comments query resolves on the server, React streams that chunk in and the fallback is replaced. Awaiting commentsPromise in NotePage instead would hold back the entire page until both queries finished.

Example · jsx
// Comments.jsx
'use client';
import { use } from 'react';

export default function Comments({ commentsPromise }) {
  const comments = use(commentsPromise); // suspends until resolved
  return (
    <ul>
      {comments.map((c) => (
        <li key={c.id}>{c.text}</li>
      ))}
    </ul>
  );
}

// page.jsx — Server Component
import { Suspense } from 'react';
import Comments from './Comments';
import db from './database';

export default async function NotePage() {
  const note = await db.notes.get('welcome');
  // Not awaited: the promise is passed down as a prop.
  const commentsPromise = db.comments.get('welcome');

  return (
    <article>
      <h1>{note.title}</h1>
      <p>{note.body}</p>
      <Suspense fallback={<p>Loading comments…</p>}>
        <Comments commentsPromise={commentsPromise} />
      </Suspense>
    </article>
  );
}

Keeping the client boundary narrow

Both versions look identical in the browser, but the second ships far less JavaScript: Sidebar and the note list are rendered on the server and arrive as serialized element trees, while only Shell's toggle logic is bundled. The rule the second version follows is that JSX passed as props or children is rendered by the parent, so it never crosses the client boundary as source code.

Example · jsx
// Bad: the whole subtree becomes client code, because Sidebar is
// imported *into* a Client Component.
'use client';
import { useState } from 'react';
import Sidebar from './Sidebar'; // now a Client Component too

export function Layout({ children }) {
  const [open, setOpen] = useState(true);
  return (
    <div>
      <button onClick={() => setOpen(!open)}>Menu</button>
      {open && <Sidebar />}
      {children}
    </div>
  );
}

// Good: the interactive shell takes server-rendered JSX as children.
// Shell.jsx
'use client';
import { useState } from 'react';

export function Shell({ sidebar, children }) {
  const [open, setOpen] = useState(true);
  return (
    <div>
      <button onClick={() => setOpen(!open)}>Menu</button>
      {open && sidebar}
      {children}
    </div>
  );
}

// page.jsx — stays a Server Component
import { Shell } from './Shell';
import Sidebar from './Sidebar';
import db from './database';

export default async function Page() {
  const notes = await db.notes.getAll();
  return (
    <Shell sidebar={<Sidebar />}>
      {notes.map((n) => (
        <p key={n.id}>{n.title}</p>
      ))}
    </Shell>
  );
}

Discussion

  • Be the first to comment on this lesson.