Type something to search...
What Are React Server Components and How Do They Work in Next.js?

What Are React Server Components and How Do They Work in Next.js?

For most of React's history, every component you wrote ended up in the browser. Even a footer with three static links shipped its JavaScript, got parsed, ran, and hydrated. Data fetching followed the same path: render a loading spinner, run useEffect, call an API route you also had to write, then render again. Next.js softened this with getServerSideProps and getStaticProps, but those worked at the page level only, and the component code still went to the client.

React Server Components change the default. A Server Component runs only on the server, can be async, can read from a database directly, and sends zero JavaScript for itself to the browser. In the Next.js App Router, every component is a Server Component unless you opt out. That single default is the biggest difference between the App Router and the Pages Router, and most confusing errors in modern Next.js projects come from not fully understanding it.

This article covers what React Server Components are, how Next.js renders them and what the RSC payload contains, how Server and Client Components differ, where the "use client" boundary goes, how to compose the two kinds of components, how to keep server-only code out of the browser, and the mistakes that cause the most common errors.

What a Server Component Is

A React Server Component (RSC) is a component that renders exclusively on the server. Its output, not its code, is sent to the client. That gives Server Components abilities a traditional React component never had:

  • They can be async and await data directly in the component body.
  • They can access server resources such as databases, the file system, environment secrets, and internal services.
  • They add nothing to the client bundle. Libraries they import, such as a Markdown parser or a date library, stay on the server.

They also give up some abilities, because there is no browser on the server and no re-rendering after the first pass:

  • No useState, useReducer, or useEffect.
  • No event handlers like onClick or onChange.
  • No browser APIs such as window, localStorage, or navigator.
  • No React Context consumption.

Here is a complete Server Component that reads from a database and renders a list. There is no API route, no loading state in useState, and no client JavaScript for this component at all:

// app/posts/page.tsx
import { prisma } from "@/lib/prisma";

export default async function PostsPage() {
  const posts = await prisma.post.findMany({
    where: { published: true },
    orderBy: { createdAt: "desc" },
    select: { id: true, title: true, excerpt: true },
  });

  return (
    <main>
      <h1>Latest posts</h1>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>
            <h2>{post.title}</h2>
            <p>{post.excerpt}</p>
          </li>
        ))}
      </ul>
    </main>
  );
}

Server Components vs. Client Components

Client Components are the React components you already know. They render on the server for the initial HTML, then hydrate and run in the browser. You opt into them with the "use client" directive at the top of a file.

CapabilityServer ComponentClient Component
Default in the App RouterYesNo
async component and direct awaitYesNo
Access databases, file system, secretsYesNo
Adds its code to the JavaScript bundleNoYes
useState, useEffect, custom hooksNoYes
Event handlers (onClick, onSubmit)NoYes
Browser APIs (window, localStorage)NoYes
Read React ContextNoYes
Rendered to HTML on the first requestYesYes

The last row trips people up. A Client Component is not client-only rendering. It still gets server-rendered to HTML for the first load, then hydrates. The difference is that its JavaScript also ships to the browser so it can become interactive.

If you are coming from the Pages Router, the earlier article on how Next.js differs from traditional React gives useful background on why server rendering mattered in the first place.

How Next.js Renders Server Components

Understanding the rendering pipeline explains almost every rule you will run into.

On the server

When a request arrives, Next.js renders the route segment by segment: the root layout, nested layouts, and the page.

  1. Server Components render first and produce the React Server Component Payload, usually called the RSC payload.
  2. Client Components are rendered to HTML using the RSC payload as input, so the first response contains complete markup.
  3. The HTML and the RSC payload are streamed to the browser.

What the RSC payload contains

The RSC payload is a compact, serialized description of the rendered tree. It contains:

  • The rendered output of every Server Component, for example the actual <li> elements with post titles filled in.
  • Placeholders marking where Client Components belong, with references to their JavaScript files.
  • The props passed from Server Components to Client Components.

It does not contain the Server Components' source code, their imports, or anything they read on the server but did not render.

On the client

On the first load, the browser:

  1. Shows the HTML immediately as a fast, non-interactive preview.
  2. Uses the RSC payload to reconcile the Server and Client Component trees.
  3. Downloads and runs the JavaScript for Client Components only, and hydrates them so event handlers work.

On later navigations with <Link>, Next.js fetches only the RSC payload for the new route, often prefetched in advance, and renders Client Components entirely in the browser. There is no full HTML document reload. This is why App Router navigation feels like a single-page app even though most components render on the server.

The "use client" Boundary

"use client" does not mark a single component. It marks a boundary in the module graph. Once a file has the directive, that file and every module it imports become part of the client bundle.

// app/ui/counter.tsx
"use client";

import { useState } from "react";

export default function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(count + 1)}>Clicked {count} times</button>
  );
}

You do not need "use client" in every file that runs on the client. Components imported by counter.tsx are already client code. You only need the directive at the entry point where server code imports client code.

The practical consequence is about bundle size: put the boundary as low in the tree as you can. If a layout is mostly static but contains one interactive search box, only the search box should be a Client Component:

// app/layout.tsx
import Logo from "./ui/logo"; // Server Component
import Search from "./ui/search"; // Client Component ("use client" inside)

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <header>
          <Logo />
          <Search />
        </header>
        <main>{children}</main>
      </body>
    </html>
  );
}

Marking the whole layout "use client" to make search work would pull the logo, the navigation, and every page rendered inside it into the client bundle. A deeper look at when to place the directive is in when to use the "use client" directive in Next.js.

Composing Server and Client Components

Passing data down as props

The most common pattern: a Server Component fetches data and passes it to a Client Component for interactivity.

// app/posts/[id]/page.tsx
import { notFound } from "next/navigation";
import { getPost } from "@/lib/data";
import LikeButton from "./like-button";

export default async function PostPage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  const post = await getPost(id);
  if (!post) notFound();

  return (
    <article>
      <h1>{post.title}</h1>
      <div>{post.body}</div>
      <LikeButton postId={post.id} initialLikes={post.likes} />
    </article>
  );
}
// app/posts/[id]/like-button.tsx
"use client";

import { useState } from "react";

export default function LikeButton({
  postId,
  initialLikes,
}: {
  postId: string;
  initialLikes: number;
}) {
  const [likes, setLikes] = useState(initialLikes);

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

Two details from Next.js 15 and later appear here: params is a Promise and must be awaited, and notFound() throws to render the nearest not-found.tsx.

Props crossing the boundary must be serializable: strings, numbers, booleans, null, plain objects, arrays, Date, Map, Set, promises, and Server Actions. Functions, class instances, and objects with methods cannot be passed. If you pass a Prisma result directly, use select to keep it to plain fields, and do not pass more fields than the client needs, because every prop is visible in the page source.

Passing Server Components as children

A Client Component cannot import a Server Component. Once you are inside the client module graph, any import becomes client code. But a Client Component can render a Server Component that it receives as children or any other prop:

// app/ui/collapsible.tsx
"use client";

import { useState } from "react";

export default function Collapsible({
  title,
  children,
}: {
  title: string;
  children: React.ReactNode;
}) {
  const [open, setOpen] = useState(false);

  return (
    <section>
      <button onClick={() => setOpen(!open)} aria-expanded={open}>
        {title}
      </button>
      {open && children}
    </section>
  );
}
// app/dashboard/page.tsx
import Collapsible from "@/app/ui/collapsible";
import RecentOrders from "./recent-orders"; // async Server Component

export default function DashboardPage() {
  return (
    <Collapsible title="Recent orders">
      <RecentOrders />
    </Collapsible>
  );
}

RecentOrders still renders on the server and can query the database. The parent page renders it, and its output is passed into the client Collapsible as a rendered result. This "slot" pattern is how you build interactive shells around server-rendered content without moving that content to the client.

Context providers

Server Components cannot read React Context, but they can render a provider that is a Client Component. Wrap the provider in its own file:

// app/providers.tsx
"use client";

import { createContext, useContext, useState } from "react";

type Theme = "light" | "dark";
const ThemeContext = createContext<{ theme: Theme; toggle: () => void } | null>(
  null,
);

export function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState<Theme>("light");
  const toggle = () => setTheme((t) => (t === "light" ? "dark" : "light"));

  return (
    <ThemeContext.Provider value={{ theme, toggle }}>
      {children}
    </ThemeContext.Provider>
  );
}

export function useTheme() {
  const ctx = useContext(ThemeContext);
  if (!ctx) throw new Error("useTheme must be used inside ThemeProvider");
  return ctx;
}
// app/layout.tsx
import { ThemeProvider } from "./providers";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <ThemeProvider>{children}</ThemeProvider>
      </body>
    </html>
  );
}

Because children is passed in, every page below the provider can still be a Server Component. Render providers as deep as practical, wrapping children rather than the whole document.

Third-party components without "use client"

Some npm packages use hooks but do not ship the "use client" directive. Importing them into a Server Component fails. The fix is a one-line wrapper:

// app/ui/carousel.tsx
"use client";

export { Carousel as default } from "acme-carousel";

Now you can use <Carousel /> from any Server Component.

Keeping Server Code on the Server

Modules can be imported from both sides of the boundary, which creates a risk: a helper that reads process.env.API_KEY could be imported by a Client Component by mistake. Next.js replaces non-NEXT_PUBLIC_ environment variables with empty strings in client bundles, so the code would fail quietly rather than leak, but it is better to make the mistake impossible.

Install the server-only package and import it at the top of any module that must never reach the browser:

// lib/data.ts
import "server-only";
import { prisma } from "@/lib/prisma";

export async function getPost(id: string) {
  return prisma.post.findUnique({
    where: { id },
    select: { id: true, title: true, body: true, likes: true },
  });
}

If a Client Component imports lib/data.ts, directly or through another module, the build fails with a clear error. The client-only package does the reverse for modules that touch window.

For more on how environment variables are exposed, see how to use environment variables in Next.js.

Data Fetching in Server Components

Because Server Components can be async, data fetching is just await. A few patterns make it fast.

Fetch in parallel when requests are independent. Sequential await calls create a waterfall:

// app/profile/[id]/page.tsx
import { getUser, getUserPosts } from "@/lib/data";

export default async function ProfilePage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;

  // Both requests start at the same time.
  const [user, posts] = await Promise.all([getUser(id), getUserPosts(id)]);

  return (
    <main>
      <h1>{user.name}</h1>
      <p>{posts.length} posts</p>
    </main>
  );
}

Stream slow parts with Suspense. Wrap a slow Server Component in <Suspense> and the rest of the page is sent immediately, with the slow section streamed in when it is ready. This is covered in detail in how to stream UI with loading.tsx and React Suspense.

Deduplicate with React cache. If a layout and a page both need the current user, wrap the getter in cache from React so it runs once per request:

// lib/user.ts
import "server-only";
import { cache } from "react";
import { prisma } from "@/lib/prisma";

export const getUser = cache(async (id: string) => {
  return prisma.user.findUnique({ where: { id } });
});

fetch calls with the same URL and options are deduplicated automatically within a request, but database clients are not, which is why cache exists.

Common Problems and Fixes

  • "You're importing a component that needs useState. This React Hook only works in a Client Component." A Server Component file uses a hook. Move the interactive part into its own file with "use client".
  • "Event handlers cannot be passed to Client Component props." A Server Component is passing an onClick function to a Client Component. Functions are not serializable. Define the handler inside the Client Component, or pass a Server Action instead.
  • "async/await is not yet supported in Client Components." You added "use client" to a file whose component is async. Remove the directive, or fetch the data in a parent Server Component and pass it as props.
  • The client bundle is huge. A "use client" directive sits too high, often on a layout or a page. Push it down to the smallest interactive leaf.
  • A Client Component imports a Server Component and it stops working. Anything imported into a client file becomes client code. Pass the Server Component as children from a Server Component parent instead.
  • Hydration mismatch warnings. A Client Component renders something different on the server and in the browser, such as Date.now() or window.innerWidth. See how to fix hydration errors in Next.js.
  • Secrets showing up in the page source. Every prop passed to a Client Component is serialized into the HTML. Select only the fields the client needs.

React Server Components FAQ

Yes, by default. Layouts, pages, and any component they import are Server Components unless the file, or a file that imports it, starts with the use client directive. The Pages Router does not use Server Components.

No, they work alongside it. Server-side rendering turns components into HTML for the first load. Server Components decide which components run only on the server and never ship JavaScript. In Next.js, both Server and Client Components are rendered to HTML on the first request.

Not by importing it. A Client Component can render a Server Component only when a Server Component parent passes it in as children or another prop. The Server Component then renders on the server and its output is placed into the client slot.

It is a compact serialized description of the rendered component tree. It contains the output of Server Components, placeholders and script references for Client Components, and the props passed to them. React uses it to build and update the page on the client.

They cannot read context. They can render a context provider that is a Client Component, and any Client Component below that provider can consume the context, while the components in between can stay Server Components.

Usually. They reduce the JavaScript sent to the browser, move data fetching closer to the data source, and allow streaming. The gains are largest on content-heavy pages where most of the UI does not need interactivity.

Conclusion

React Server Components flip React's default from "everything ships to the browser" to "nothing ships unless it needs to." In the Next.js App Router, components run on the server, fetch data with plain await, and send only their rendered output inside the RSC payload. Client Components are the opt-in for state, effects, event handlers, and browser APIs, and the "use client" directive marks the point where the module graph crosses into the browser.

The patterns that matter most are simple: keep the client boundary at the leaves, pass data down as serializable props, pass Server Components into Client Components as children, wrap context providers in their own client file, and protect server modules with server-only. With those habits, most of your app stays on the server, and the JavaScript you do ship is only what makes the page interactive.

Here are some useful references for going deeper on React Server Components:

  1. Next.js Docs: Server and Client Components — how Next.js renders and composes both kinds of components.
  2. React Docs: Server Components — the React specification of Server Components.
  3. React Docs: 'use client' directive — how the client boundary works and what props are serializable.
  4. Next.js Docs: Fetching Data — data fetching, parallel requests, and streaming in Server Components.
  5. npm: server-only — the package that prevents server modules from being imported into the client.
Tags :
Share :

Related Posts

Building Powerful Desktop Applications with Next.js

Building Powerful Desktop Applications with Next.js

Next.js is a popular React framework known for its capabilities in building server-side rendered (S

Continue Reading
Can use custom server logic with Next.js?

Can use custom server logic with Next.js?

Next.js, a popular React framework for building web applications, has gained widespread adoption for its simplicity, performance, and developer-frien

Continue Reading
TypeScript with Next.js?

TypeScript with Next.js?

Next.js has emerged as a popular React framework for building robust web applications, offering developers a powerful set of features to enhance thei

Continue Reading