React Server Components (RSC) change where your components run. Frameworks like the Next.js App Router use them by default, so understanding the split between server and client is now a core React skill.
The problem they solve
In a classic single-page React app, the browser downloads a JavaScript bundle, runs it, then fetches data from an API, then renders the page. Every library used for rendering, from a date formatter to a Markdown parser, ships to every visitor. Data loading often turns into a waterfall: the page loads, then a component fetches the user, then a child fetches their posts.
Server components move that work to the server. The component runs where the data lives, fetches what it needs, and sends the browser a finished description of the UI. Libraries used only on the server never reach the browser at all. The browser still gets React for the interactive parts, but less code overall.
Server components
- They run only on the server, at build time or per request.
- They can be async and fetch data directly, for example from a database, without an API layer in between.
- Their code is never sent to the browser, which keeps bundles smaller.
- They cannot use state, effects or browser-only APIs.
Client components
Add the “use client” directive at the top of a file to make it a client component. Client components are still pre-rendered to HTML, but they also ship JavaScript to the browser so they can use useState, useEffect, event handlers and browser APIs.
// app/page.tsx (server component)
import { LikeButton } from "./like-button";
export default async function Page() {
const post = await getPost();
return (
<article>
<h1>{post.title}</h1>
<LikeButton postId={post.id} />
</article>
);
}
// app/like-button.tsx (client component)
"use client";
import { useState } from "react";
export function LikeButton({ postId }: { postId: number }) {
const [liked, setLiked] = useState(false);
return <button onClick={() => setLiked(!liked)}>{liked ? "Liked" : "Like"}</button>;
}
Rules of thumb
- Keep components on the server by default and add “use client” only where you need interactivity.
- Push client components down the tree: make the button interactive, not the whole page.
- Props passed from a server component to a client component must be serializable, so pass data, not functions or class instances.
- Never import server-only code, such as database clients or secrets, into a client component.
Passing server components into client components
A client component can’t import a server component, because everything a client file imports becomes client code. But it can receive one as a prop. A common case is a client-side layout, such as a collapsible sidebar, that wraps server-rendered content:
// app/layout.tsx (server component)
import { Sidebar } from "./sidebar"; // "use client"
import { RecentPosts } from "./recent-posts"; // server component
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
<Sidebar>
<RecentPosts />
</Sidebar>
{children}
</>
);
}
RecentPosts still runs on the server and can query the database. The sidebar only receives its already-rendered output through children, so the open and close logic stays in the browser while the data stays on the server.
Server components vs server-side rendering
These are easy to confuse. Server-side rendering (SSR) turns components into HTML on the server for the first page load, then the browser downloads all the same component code and runs it again to make the page interactive, a step called hydration. Server components are never hydrated. Their code stays on the server, and only their result is sent. In the App Router you get both: server components for most of the tree, and SSR for the HTML of the client components inside it.
Common mistakes
- Adding “use client” to the root layout to fix one error, which turns the whole app into client code.
- Using a context provider in a server component. Providers need “use client”; wrap them in a small client file and keep the rest on the server.
- Expecting a server component to re-render on its own. To show fresh data, refresh the route or revalidate it after a change.
What we think
Server Components are the biggest change to how React apps are built in years, and for content-heavy sites they are a clear improvement: less JavaScript for visitors and simpler data loading for developers.
The learning curve is real, mostly in knowing where the server-client boundary sits. Our advice is to start every component on the server, add “use client” only where a user actually interacts, and keep those client pieces small. Most of the confusing bugs come from crossing that line carelessly.