Server Components and Frameworks
Every component you’ve written so far in this course runs in the browser: React renders it, hydrates it, and re-renders it whenever state changes. React Server Components (RSC) introduce a second kind of component that never runs in the browser at all — it renders once on the server (or at build time), sends only the resulting output to the client, and ships zero JavaScript for itself. This matters because it lets you fetch data, read files, or talk to a database directly inside a component, without an API layer, a loading spinner, or any client-side bundle cost. RSC is not something you turn on in a plain Vite app, though — it requires a framework that implements the RSC protocol, and today that mostly means Next.js (App Router).
Overview / How it works
Historically, every React component was a Client Component: React renders it once on the server for the initial HTML (if you use server-side rendering), then ships the component’s JavaScript to the browser, where React "hydrates" it — attaches event listeners and takes over future renders. The component’s code exists in two places: it runs once on the server to produce HTML, and it runs again in the browser to become interactive.
A Server Component only ever runs on the server. There is no second run in the browser, and its code is never included in the JavaScript bundle sent to the client. React renders the Server Component to a special streaming format (an RSC payload, not plain HTML), the framework converts that into HTML for the first paint, and the browser only receives the finished markup — plus whatever JavaScript is needed for any Client Components nested inside it. This is why Server Components can safely do things a browser component never could: query a database, read environment secrets, use large parsing libraries, or call `await fetch(…)` directly in the component body, all without leaking that code or those credentials to the client.
In a framework like Next.js’s App Router, every component inside the `app/` directory is a Server Component by default. You opt a specific file into being a Client Component by adding the `"use client"` directive as the very first line of the file. Once a file is marked `"use client"`, that component (and everything it imports) is bundled for the browser and hydrates like a normal React component you already know — it can use `useState`, `useEffect`, `onClick`, refs, browser APIs, all of it. Server Components cannot use any of those, because there is no browser session for them to run a second time in.
The two component types compose in one direction: a Server Component can render a Client Component and pass it props, but a Client Component cannot `import` a Server Component directly (doing so would just make it a Client Component too). Instead, Server Components can be passed into Client Components as `children` or other JSX props — a pattern often called the "server in client" slot. Because props crossing the server-to-client boundary have to be serialized and sent over the wire, they must be plain, serializable values: strings, numbers, booleans, plain objects, arrays, and JSX — never functions, class instances, or Symbols (with one deliberate exception: Server Actions, functions marked `"use server"`, which is a related but separate feature for form submissions and mutations).
| Server Component | Client Component |
|---|---|
| Runs only on the server / at build time | Runs on the server for first paint, then again in the browser |
| Ships no JavaScript to the browser | Ships its code as part of the client bundle |
| Can `await` data directly (fetch, database, filesystem) | Fetches with `useEffect` or a data library |
| Cannot use `useState`, `useEffect`, event handlers | Can use all hooks and event handlers |
| No directive needed (default in `app/`) | Requires `"use client"` at the top of the file |
Syntax
There is no new hook or component API to learn — Server Components use the exact same function-component syntax you already know. The only new piece of syntax is the directive that marks a file as a Client Component:
"use client";
import { useState } from "react";
export default function MyComponent() {
// hooks and event handlers are allowed here
}
- `"use client"` — a string literal, must be the first line of the file (before imports). Marks this file and everything it imports as part of the client bundle.
- No directive — the default. Any file without `"use client"` inside `app/` is treated as a Server Component.
- `async function Component()` — Server Components are allowed to be `async` functions, so you can `await` data fetching directly in the component body. Client Components cannot be `async` function components.
- `"use server"` — a related but different directive, used inside a function to mark it as a Server Action (for form submissions and mutations). It is not required to understand Server Components and is covered separately.
Examples
Example 1: A Server Component that fetches data directly
// app/posts/page.jsx
async function getPosts() {
const res = await fetch("https://api.example.com/posts", {
cache: "no-store",
});
if (!res.ok) {
throw new Error("Failed to fetch posts");
}
return res.json();
}
export default async function PostsPage() {
const posts = await getPosts();
return (
<main>
<h1>Latest Posts</h1>
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</main>
);
}
Output: Renders a heading "Latest Posts" followed by a bulleted list of post titles. This component has no `"use client"` directive, so it never runs in the browser: the `fetch` call happens on the server, and the browser receives finished HTML with the list already filled in — no loading spinner, no client-side network request, no JavaScript shipped for this component at all.
Example 2: A Client Component for interactivity
// app/components/LikeButton.jsx
"use client";
import { useState } from "react";
export default function LikeButton({ initialLikes }) {
const [likes, setLikes] = useState(initialLikes);
return (
<button onClick={() => setLikes(likes + 1)}>
❤️ {likes} likes
</button>
);
}
Output: Renders a button reading, for example, "❤️ 3 likes" (using the `initialLikes` prop). Clicking it increments the number shown, entirely in the browser, without a page reload. Because this file starts with `"use client"`, its code — including `useState` and the click handler — is bundled and sent to the browser to hydrate.
Example 3: Composing a Server Component with a Client Component
// app/posts/[id]/page.jsx
import LikeButton from "../../components/LikeButton";
async function getPost(id) {
const res = await fetch(`https://api.example.com/posts/${id}`, {
cache: "no-store",
});
return res.json();
}
export default async function PostPage({ params }) {
const post = await getPost(params.id);
return (
<article>
<h1>{post.title}</h1>
<p>{post.body}</p>
<LikeButton initialLikes={post.likes} />
</article>
);
}
Output: Renders the post’s title and body as static server-rendered HTML, plus a working, clickable "❤️ N likes" button. `PostPage` is a Server Component: it fetches the post directly with `await` and never ships its own code to the browser. It renders `LikeButton`, a Client Component, and hands it a single serializable number (`post.likes`) as a prop — exactly the kind of value that’s allowed to cross the server/client boundary. Only `LikeButton`’s JavaScript is sent to the browser; the surrounding article markup is not.
How it works step by step / Under the hood
On the initial request: the framework’s server starts rendering the route’s Server Components top-down. Any `await` inside a Server Component (a fetch call, a database query) pauses that component’s rendering until the data resolves. As Server Components finish, React serializes their output into an RSC payload — a compact, streamable description of the resulting tree, including placeholders for any Client Components. The framework uses that payload to produce real HTML, which streams to the browser as it becomes ready (this is why you can wrap slow parts of a Server Component tree in `<Suspense fallback={…}>` — React streams the fallback first, then swaps in the real content when the data arrives, without blocking the rest of the page).
In the browser: the HTML paints immediately, giving the user something to look at right away. Then React hydrates only the Client Components found in the payload — attaching event listeners and wiring up `useState`/`useEffect` for `LikeButton` in Example 3, for instance — while the surrounding Server Component output (the article title and body) stays as plain, inert HTML that React never touches again.
On a state update: only Client Components re-render the normal React way (state change → re-render → reconcile → commit), exactly as in every other lesson in this course. Server Components do not re-render in the browser at all — to get fresh server data (e.g., after a form submission), the framework re-runs the relevant Server Components on the server and streams a new payload down, a mechanism frameworks expose through APIs like `router.refresh()` or Server Actions.
Common Mistakes
Mistake 1: Using state or event handlers without "use client"
// app/components/Counter.jsx — WRONG
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
Without a `"use client"` directive, this file is treated as a Server Component. `useState` and `onClick` have no meaning on the server, so the framework throws a build/runtime error explaining that this hook only works in a Client Component. Fix it by marking the file as a Client Component:
// app/components/Counter.jsx — CORRECT
"use client";
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
Mistake 2: Passing a function prop from a Server Component to a Client Component
// WRONG — defined in a Server Component, passed to a Client Component
export default async function PostPage({ params }) {
const post = await getPost(params.id);
function handleLike() {
console.log("liked!"); // this only runs on the server
}
return <LikeButton onLike={handleLike} />;
}
Functions are not serializable, so they cannot be sent across the server/client boundary as ordinary props — the framework throws an error at render time. The fix is to keep the interactive logic entirely inside the Client Component itself (as in Example 2), or, for mutations that truly need to run on the server, use a dedicated Server Action (a function marked `"use server"`) instead of an ordinary callback prop.
Best Practices
- Default to Server Components everywhere; add `"use client"` only to the specific files that truly need state, effects, or browser APIs.
- Push `"use client"` as far down the tree as possible — wrap just the interactive leaf (a button, a form) rather than an entire page, so the rest of the tree stays free of client JavaScript.
- Fetch data with `await` directly inside Server Components instead of `useEffect` + `fetch`, which avoids client-side waterfalls and loading spinners for data you already have on the server.
- Only pass serializable values (strings, numbers, booleans, plain objects/arrays, JSX) as props from a Server Component into a Client Component.
- Wrap slow data-dependent Server Components in `<Suspense>` so the rest of the page can stream in without waiting on the slowest fetch.
- Remember that `"use client"` means "this code ships to the browser," not "this only renders in the browser" — Client Components are still rendered on the server for the first HTML paint, then hydrated.
- Use a framework that actually implements the RSC spec (Next.js App Router is the production-ready choice) rather than trying to hand-roll Server Components in a plain client-only bundler setup.
Practice Exercises
- Create a Server Component (no directive needed) that fetches a list of users from a public API and renders their names in a `<ul>`. Confirm the data appears in the initial HTML with no loading state.
- Add a small `"use client"` component next to each user that toggles showing their email address with `useState`, and render it from inside the Server Component you just built.
- Intentionally add `useState` to a component with no `"use client"` directive, run it, and read the resulting error message. Then fix it by extracting the stateful logic into its own Client Component file.
Summary
- Server Components run only on the server, ship no JavaScript to the browser, and can `await` data fetching directly in the component body.
- Client Components are marked with `"use client"` at the top of the file and behave like the React you already know: hooks, state, and event handlers all work.
- Server Components render by default inside a framework’s app directory; Client Components opt in explicitly.
- A Client Component cannot import a Server Component, but a Server Component can render a Client Component and pass it serializable props.
- Only plain, serializable values can cross the server/client boundary as props — never functions or class instances.
- Server Components require a framework that implements the RSC protocol, such as Next.js App Router; they are not available in a plain client-only React setup.
