Quick Answer

React Server Components (RSCs) run only on the server and send rendered output to the browser, never their code. They can be async and read a database or filesystem directly, adding zero JavaScript to the bundle. Components that need state, effects, or event handlers opt into the browser with the "use client" directive. In frameworks like the Next.js App Router, every component is a Server Component unless marked otherwise.

Two kinds of components now

Before RSCs, every React component was the same kind of thing: it shipped to the browser as JavaScript and ran there. Server-side rendering existed, but the server only produced HTML and then sent the same component code to the client to hydrate.

React Server Components split components into two categories:

  • Server Components - run on the server, at build time or per request, produce a serialized description of UI, and send that. Their code never reaches the browser. This is the default in the Next.js App Router.
  • Client Components - the components you already know. They render on the server for the initial HTML and ship to the browser to become interactive. You opt in by putting "use client" at the top of the file.

The goal is to make "no JavaScript" the default for the large share of a typical UI that just displays data, and to pay for JavaScript only where there is real interactivity.

What a Server Component can and cannot do

A Server Component can be an async function, which changes how you fetch data:

// app/page.tsx - a Server Component
import { getUser } from './db';

export default async function Profile() {
  const user = await getUser();   // runs on the server
  return <h1>Welcome, {user.name}</h1>;
}

No useEffect, no loading state, no client-side fetch waterfall. The function reads data - a database query, a file, an internal API with a secret key - and returns UI. The credentials and the query never leave the server.

What a Server Component cannot do: use useState, useReducer, useEffect, or any hook that depends on a persistent component instance; attach event handlers such as onClick; or touch browser APIs like window and localStorage. Import useState into one and the build fails with: "You're importing a module that depends on useState into a React Server Component module. This API is only available in Client Components."

The "use client" boundary

"use client" is a directive - a string literal at the very top of a file, before the imports:

"use client";
import { useState } from 'react';

export default function Counter({ start }) {
  const [count, setCount] = useState(start);
  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

It marks a boundary, not just one file. Every module that file imports, and every component rendered inside it, becomes part of the client bundle from that point down - unless a Server Component is passed into it as a prop.

Two things people misread:

  • "use client" does not mean "client-only". A Client Component still renders on the server for the initial HTML - confirmed by finding its text in the prerendered HTML of a build. It just also runs in the browser.
  • You do not put "use client" on every component. Put it on the leaves - the actual interactive widgets - and keep pages and layouts as Server Components. Marking a top-level layout as a client component drags the entire tree below it into the browser bundle.

What actually ships to the browser

Here is the concrete payoff, confirmed by inspecting a production build. Put a unique string inside a Server Component's data function and build the app:

  • The string appears under .next/server/ - in the server-rendered HTML and the RSC payload.
  • It appears in none of the JavaScript files under .next/static/ that the browser downloads.

The same string placed inside a Client Component does turn up in a .next/static/chunks/ file. Server Component code - including any library it imports, such as a date formatter or a markdown parser - is simply absent from the bundle. A heavy dependency used only for server rendering costs the user nothing.

What the browser receives for a Server Component is the RSC payload: a compact, streamed description of the rendered output - element types, props, and slots where Client Components go - not JSX and not plain HTML. React uses it to build the tree on the client and to reconcile after navigation without re-running any server code.

Passing props across the boundary

Props passed from a Server Component to a Client Component cross a serialization boundary, so they must be serializable: strings, numbers, booleans, plain objects and arrays of those, Date, Map, Set, and JSX elements. What cannot cross:

  • Functions, including event handlers. This build error is one every beginner hits:
    Error: Event handlers cannot be passed to Client Component props.
      {onClick: function onClick}
    If you need interactivity, consider converting part of this
    to a Client Component.
    The fix is to define the handler inside the Client Component, not pass it in. Server Actions - functions marked "use server" - are the one deliberate exception and can be passed as props.
  • Class instances with methods, and anything else carrying behaviour rather than data.

Data flows one way: Server Component to Client Component, through props. A Client Component cannot import a Server Component and render it directly - the import would pull server code into the browser bundle.

Server components inside client components

The pattern that keeps this composable: a Client Component accepts children (or any JSX prop) and a Server Component fills the slot.

// ClientShell.tsx
"use client";
export default function ClientShell({ children }) {
  const [open, setOpen] = useState(true);
  return open ? <div>{children}</div> : null;
}

// page.tsx - a Server Component
export default function Page() {
  return (
    <ClientShell>
      <ServerInfo />   {/* still a Server Component */}
    </ClientShell>
  );
}

ServerInfo renders on the server; its already-rendered output is slotted into ClientShell as children. ClientShell only positions that content - it never sees the server code. Confirmed in a build: the server component's marker string stays out of the client chunks even though its parent in the tree is a Client Component.

This is how you wrap server-rendered content in interactive shells - tabs, accordions, modals - without losing the zero-bundle benefit.

Frequently Asked Questions

Are React Server Components the same as server-side rendering? No. SSR renders your components to HTML on the server and then sends the same component JavaScript to the browser to hydrate. Server Components never send their code at all, only a description of their output, and they can run async data fetching directly.
Do I need Next.js to use Server Components? In practice, yes, or another framework with RSC support. RSCs need a bundler and server integration that understands the server and client split. React Router is adding support, but React does not ship a standalone way to run RSCs.
Does "use client" make a component render only in the browser? No. A Client Component still renders on the server for the initial HTML, then hydrates and runs in the browser. The directive marks where the client bundle boundary begins, not where rendering happens.
Why can't I pass an onClick handler to a Client Component from a Server Component? Functions are not serializable, and props crossing that boundary are serialized. Define the handler inside the Client Component. Server Actions, marked with "use server", are the intentional exception.
Can a Server Component be a child of a Client Component? Yes, if it is passed in as children or another JSX prop. A Client Component cannot import a Server Component directly, because that would bundle server code for the browser.