Skip to content
React rc advanced 5 min read

React Server Components

React Server Components (RSC) let you render components entirely on the server, sending the resulting UI to the browser without shipping their JavaScript to the client. This shrinks bundle size, moves data fetching closer to your data sources, and keeps secrets like API keys and database credentials off the client. RSC is a foundational shift: instead of “render everything in the browser,” you decide per-component where the code runs.

Server components vs client components

By default, every component in a React Server Components environment (like the Next.js App Router) is a server component. Server components run only on the server. Their code is never sent to the browser, they can be async, and they can read directly from a database or filesystem. What they cannot do is use state, effects, or browser-only APIs, because there is no client runtime backing them.

A client component is the React you already know: it runs in the browser, supports useState, useEffect, event handlers, and refs. You opt a component into the client by placing the "use client" directive at the very top of the file.

"use client";

import { useState } from "react";

export function LikeButton({ initialLikes }) {
  const [likes, setLikes] = useState(initialLikes);

  return (
    <button onClick={() => setLikes((n) => n + 1)}>
{likes}
    </button>
  );
}

The "use client" directive marks the boundary: this file and everything it imports become part of the client bundle. Everything above that boundary stays on the server.

CapabilityServer componentClient component
Ships JS to browserNoYes
useState / useEffectNoYes
Event handlers (onClick)NoYes
async/await in componentYesNo
Direct DB / filesystem accessYesNo
Access to secrets / env keysYes (safe)No (exposed)

Async server components for data

Because server components run on the server, they can be declared async and await data directly inside the component body. There is no useEffect, no loading-flag juggling, and no client-side waterfall — the data is resolved before the HTML is produced.

// app/users/[id]/page.jsx — a server component (no "use client")
import { LikeButton } from "./LikeButton";

async function getUser(id) {
  const res = await fetch(`https://api.example.com/users/${id}`);
  if (!res.ok) throw new Error("Failed to load user");
  return res.json();
}

export default async function UserPage({ params }) {
  const user = await getUser(params.id);

  return (
    <article>
      <h1>{user.name}</h1>
      <p>{user.bio}</p>
      <LikeButton initialLikes={user.likes} />
    </article>
  );
}

Notice how a server component (UserPage) imports and renders a client component (LikeButton) and passes data to it as props. This is the normal composition pattern: fetch on the server, hand the result to an interactive island on the client.

Tip: Do data fetching in server components and keep "use client" components small and leaf-like. The deeper you push the client boundary, the less JavaScript the user downloads.

The serialization boundary

When a server component passes props to a client component, those props must cross from the server to the browser. React serializes them into a special streaming format, so the values must be serializable. Plain objects, arrays, strings, numbers, dates, and even Promises and JSX elements can cross the boundary. Functions, class instances, and closures cannot.

// ✅ Works — serializable data
<LikeButton initialLikes={42} user={{ name: "Ada" }} />

// ❌ Throws — functions can't be serialized to the client
<LikeButton onLike={() => saveToDatabase()} />

If you try to pass a non-serializable value, React reports it at build/render time:

Output:

Error: Functions cannot be passed directly to Client Components
unless you explicitly expose it by marking it with "use server".

The exception is server actions: functions marked with the "use server" directive can be passed to client components and invoked from the browser, with React wiring up the network call for you.

// app/actions.js
"use server";

export async function saveToDatabase(formData) {
  const name = formData.get("name");
  await db.users.create({ name });
}
"use client";

import { saveToDatabase } from "./actions";

export function NameForm() {
  return (
    <form action={saveToDatabase}>
      <input name="name" />
      <button type="submit">Save</button>
    </form>
  );
}

Where they run: the Next.js App Router

RSC is not something you enable in a plain Vite SPA — it requires a framework that implements the server runtime and bundler integration. The reference implementation today is the Next.js App Router (the app/ directory), where files are server components by default. Other frameworks such as Remix and Waku are adopting RSC as the model matures.

A typical request flows like this:

  1. The browser requests a route.
  2. Server components run on the server, fetch data, and render to the RSC payload (a serialized description of the UI).
  3. React streams that payload to the browser, progressively flushing chunks as they become ready (which pairs naturally with <Suspense>).
  4. The client React runtime hydrates only the client components, attaching interactivity to the islands.

Because server component code never reaches the browser, large dependencies used only for rendering — a Markdown parser, a date formatter, an SDK — add zero to the client bundle.

Warning: A common mistake is adding "use client" to a high-level layout. That marks the entire subtree as client code and forfeits the server-rendering benefits. Keep the directive on the smallest interactive components.

Best Practices

  • Default to server components; only reach for "use client" when you need state, effects, or event handlers.
  • Push the client boundary as far down the tree as possible to minimize shipped JavaScript.
  • Fetch data inside async server components rather than in client-side effects to avoid request waterfalls.
  • Pass only serializable props across the boundary; use server actions ("use server") for callbacks the client must trigger.
  • Wrap slow server components in <Suspense> so React can stream a fallback while data resolves.
  • Never read secrets or env keys in client components — keep that logic in server components where it stays private.
Last updated June 14, 2026
Was this helpful?