
Published July 16, 2026 · 10 min read
React Server Components are most useful when they make an execution boundary obvious. Data access, secrets, and non-interactive rendering can stay on the server, while focused Client Components provide state, effects, event handlers, and browser APIs where the interface needs them.
The practical question is not “Does this application use RSC?” It is “Which parts of this route need to execute in the browser?” In the Next.js App Router, pages and layouts begin as Server Components, so the useful default is to keep that server boundary and move only the interactive leaves into the client graph.
That does not make Server Components a synonym for request-time server-side rendering, and it does not guarantee a smaller or faster application. They can run during a build or for a request. The result depends on the boundary you draw, the dependencies pulled into the client graph, and the server work introduced on the other side.
The Next.js Server and Client Components guide starts with capabilities rather than preferences. The server and browser are different environments, so each should own the work it can perform safely and efficiently.
A Server Component can fetch from a database or API close to the data source, use credentials that must not enter the browser bundle, and render without shipping its component implementation to the client. It can also participate in cached or streamed rendering when the route needs those behaviors.
Server Component does not mean “runs for every request.” React supports running them ahead of time, including during a build, while a framework decides when and where that work happens. It is also different from conventional SSR: a Client Component may be prerendered into the initial HTML and still ship JavaScript for hydration, while a Server Component sends its rendered result instead of its implementation.
Use a Client Component when the UI needs state, lifecycle logic, event handlers, custom hooks that depend on client APIs, or browser capabilities such as localStorage, geolocation, media, or the DOM. The "use client" directive creates the entry point for that client-side module graph.
| Responsibility | Server Component | Client Component |
|---|---|---|
| Database or privileged service access | Yes, behind authorization and a safe data layer | No; call a server boundary instead |
| Secrets and server environment variables | Yes, but never serialize them into output | No |
| State, effects, and event handlers | No | Yes |
| Browser APIs | No | Yes |
| Component implementation shipped to the browser | No | Yes |
| Initial HTML in Next.js | Contributes rendered output | Can be prerendered, then hydrated |
The goal is not to eliminate Client Components. It is to make browser execution intentional.
Understanding the rendering pipeline makes the boundary less mysterious. Next.js does not send only HTML or only a component tree; it coordinates several outputs.
On the server, React renders Server Components into the React Server Component payload. The payload contains their rendered result, placeholders and JavaScript references for Client Components, and the props passed across the boundary. Next.js uses that payload together with Client Components to prerender the route's HTML.
On the first browser load:
A Server Component does not hydrate because its implementation is not part of the browser bundle. Client Components do hydrate, even though Next.js can include their initial output in the server-generated HTML.
For later client-side navigations, Next.js can prefetch and cache the RSC payload. The browser uses that payload to update the route, while Client Components render on the client without receiving a new server-rendered HTML document for the navigation.
This is why RSC is more than an HTML-rendering technique. The payload preserves the relationship between server-rendered output and interactive client entries across navigations.
Suppose a post page is mostly data and prose, with one interactive like control. The page can remain a Server Component and pass only the control's initial value into a client island:
import LikeButton from "@/app/ui/like-button"
import { getPost } from "@/lib/posts"
export default async function PostPage({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
const post = await getPost(id)
return (
<article>
<h1>{post.title}</h1>
<p>{post.summary}</p>
<LikeButton initialLikes={post.likes} />
</article>
)
}The button owns the state and event handler, so its file defines the client boundary. This example increments local display state only; persisting a real like would also need a validated, authorized server mutation:
"use client"
import { useState } from "react"
export default function LikeButton({ initialLikes }: { initialLikes: number }) {
const [likes, setLikes] = useState(initialLikes)
return (
<button type="button" onClick={() => setLikes((value) => value + 1)}>
{likes} likes
</button>
)
}"use client" marks a module and its transitive dependencies as client code. It does not merely label one function. If the post page carried the directive, its imports could join the client graph even though most of the route has no browser responsibility.
You also do not need to repeat the directive in every descendant. Add it to the files exported directly across a Server Component boundary, then let ordinary imports form the client subtree.
Crossing from a Server Component into a Client Component is an application boundary. Treat the values sent through it as a public UI contract, not as a convenient place to forward everything returned by the database.
Props passed into a Client Component must use types React can serialize. “Serializable” is not identical to “valid JSON”: React supports values such as Date, Map, Set, promises, JSX, and Server Functions in addition to common primitives, arrays, and plain objects. It does not support arbitrary class instances or ordinary server functions.
Technical support is not the same as good boundary design. Prefer a small view model containing only what the interactive UI needs. The LikeButton receives one number rather than a database record with author details, moderation fields, and internal identifiers. A smaller contract is easier to review, test, cache, and change.
Remember that rendered HTML, RSC payload data, and Client Component props reach the browser. A value is not secret merely because a Server Component read it.
Modules can be imported from both server and client graphs. Mark data-access modules with server-only so Next.js produces a build-time error if one crosses into client code:
import "server-only"
export async function getPost(id: string) {
const response = await fetch(`https://internal.example.com/posts/${id}`, {
headers: { authorization: `Bearer ${process.env.API_TOKEN}` },
})
if (!response.ok) {
throw new Error("Post request failed")
}
const post = await response.json()
return {
title: post.title,
summary: post.summary,
likes: post.likes,
}
}Validate untrusted service data in production and enforce authorization before returning the view model. The server-only guard prevents accidental environment mixing; it does not replace input validation, access control, or output review.
A Client Component can visually contain server-rendered UI without importing that Server Component into its own module graph. Composition is the key.
Create a client-side modal that owns only visibility state and accepts a slot:
"use client"
import { useState, type ReactNode } from "react"
export default function Modal({ children }: { children: ReactNode }) {
const [open, setOpen] = useState(false)
return (
<section>
<button type="button" onClick={() => setOpen((value) => !value)}>
{open ? "Close cart" : "Open cart"}
</button>
{open ? children : null}
</section>
)
}The parent Server Component renders the data-dependent child and passes its output through children:
import Cart from "./ui/cart"
import Modal from "./ui/modal"
export default function Page() {
return (
<Modal>
<Cart />
</Modal>
)
}Modal never imports Cart. React renders Cart on the server first, and the RSC payload describes where that rendered result belongs inside the Client Component.
React context is not supported inside Server Components. Put the provider in a focused Client Component, then render it from a Server Component:
import ThemeProvider from "./theme-provider"
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html>
<body>
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
)
}The Next.js guidance recommends rendering providers as deep as practical. Wrapping {children} instead of the complete document leaves more of the surrounding Server Component tree available for static optimization.
If a third-party component uses hooks or browser APIs but does not expose its own client entry, create one narrow adapter:
"use client"
export { Carousel as default } from "acme-carousel"The rest of the application can now import that adapter from a Server Component without marking the whole route as client code. Library authors should place "use client" on interactive entry points so consumers do not need this wrapper.
A well-drawn boundary can remove component implementations and server-only dependencies from the browser bundle. That may reduce download, parse, and evaluation work on slower devices. Rendering data during the server pass can also avoid a client Effect whose first job is to request the data after hydration, and streaming can reveal useful content before every route dependency is ready.
Those are potential outcomes, not automatic wins. A small-looking Client Component can import a large editor, charting library, or design-system subtree. Inspect production bundles and interaction traces rather than counting "use client" directives.
Work also moves to the server. Rendering and data access can increase compute, database concurrency, regional latency, and the number of failure modes a team must observe. A route may now involve server logs, the RSC payload, prerendered HTML, hydrated client code, caches, and later mutations. Operational ownership matters as much as component syntax.
For a company, the useful question is whether the architecture improves a product outcome at an acceptable delivery cost. Measure transferred JavaScript, interaction latency, server time, database load, error rates, and real-user behavior before and after moving a boundary. If the change only renames components while adding framework complexity, the simpler architecture is better.
"use client" entry; they form the client module graph.server-only.Start with the environment each responsibility needs. Render data-rich, non-interactive UI in Server Components, and add the smallest useful Client Components for state and browser behavior. Keep the contract between them deliberate, pass server-rendered content through composition, and guard privileged modules from the client graph.
React Server Components are valuable when that split removes real browser work without creating unacceptable server latency or operational complexity. The boundary is the feature; the label is not. Measure both sides, and keep the simpler system whenever the evidence does not justify the extra machinery.
Occasional articles on React, full-stack development, performance, and AI workflows.