Why Most React Developers Pick the Wrong Component Type (And How to Stop)
You should use React Server Components for data fetching, accessing backend resources, and rendering static or server-driven UI — and reserve Client Components for interactive elements that require state, event handlers, or browser APIs. The fundamental rule is straightforward: if your component does not need useState, useEffect, onClick, or any browser-only API, it belongs on the server. This single decision determines whether your users download 5KB of JavaScript or 50KB, whether your page renders in 800 milliseconds or 3 seconds, and whether your application architecture scales cleanly or collapses under its own complexity.
The distinction between React Server Components and Client Components is not academic. It is the most consequential architectural decision a React developer makes in 2026, and getting it wrong costs real performance, real money, and real developer time. This guide provides the definitive framework for making that decision correctly — every time.
What React Server Components Actually Are
React Server Components (RSCs) are a component type that renders entirely on the server and never ship their JavaScript to the browser. They execute on the server — either at build time or on each request — produce a rendered output, and that output is streamed to the client as a compact binary format called the React Server Component Payload. The client never sees the original component code. It only sees the result.
This is not Server-Side Rendering (SSR) in the traditional sense. SSR converts React components to HTML on the server on each request, but the component JavaScript still ships to the client for hydration. RSCs eliminate the component JavaScript from the client bundle entirely. The component runs, renders, and its code is discarded from the client’s perspective.
Consider the difference concretely. A traditional SSR page that renders a markdown document using marked and sanitize-html still sends both libraries — approximately 75KB gzipped — to the client. With Server Components, those libraries execute on the server, render the markdown to HTML, and the client receives only the resulting markup. The 75KB of library code never enters the client bundle.
RSCs are stable in React 19 and form the default rendering model in Next.js App Router applications. They represent the most significant architectural shift in React since hooks.
The Zero-Bundle-Size Advantage
The defining promise of Server Components is zero bundle-size impact. Any dependency imported in a Server Component is excluded from the client JavaScript bundle. This is not an optimization that requires configuration — it is the fundamental operating model.
The practical implications are substantial:
- Heavy data transformation libraries (date manipulation, markdown parsing, syntax highlighting, PDF generation) execute on the server and vanish from the client payload.
- Database clients and ORM layers run server-side without creating separate API routes.
- Secret keys, API tokens, and environment variables remain securely on the server. Server Components can directly access process.env without fear of client exposure.
- Large third-party components that are purely presentational (charts, rich text renderers, document viewers) contribute nothing to the JavaScript bundle.
The result is measurable. Applications that migrate from client-heavy architectures to Server Component-first patterns routinely report 30 to 60 percent reductions in client JavaScript bundle size. For users on constrained networks or devices, this is the difference between a usable application and an abandoned one.
When to Use ‘use client’
The "use client" directive marks the boundary between the server module graph and the client module graph. Placing it at the top of a file declares that everything in that file — and everything it imports — belongs to the client bundle.
Use "use client" when your component requires any of the following:
- State management — useState, useReducer, or any custom hook that manages reactive state.
- Event handlers — onClick, onChange, onSubmit, onKeyDown, or any user interaction callback.
- Lifecycle effects — useEffect, useLayoutEffect, or any hook that runs side effects in response to render cycles.
- Browser-only APIs — localStorage, sessionStorage, window, document, navigator.geolocation, IntersectionObserver, MutationObserver, or any API that only exists in a browser environment.
- Refs with DOM interaction — useRef when used to manipulate a DOM node directly (focusing inputs, measuring elements, triggering animations).
- Third-party libraries that depend on the DOM — animation libraries like Framer Motion, charting libraries that require canvas access, or drag-and-drop systems.
The critical architectural insight is that "use client" creates a boundary, not a label. When you mark a component with "use client", you are drawing a line in your module tree. Every component imported below that line becomes part of the client bundle, regardless of whether those child components individually need client-side features. This makes placement of the boundary a first-order performance decision.
The strategic principle: push the "use client" boundary as deep into your component tree as possible. Mark the smallest, most specific interactive components as Client Components, and let everything above them remain server-rendered.
Data Fetching Patterns
Server Components fundamentally change how React applications fetch data. The old paradigm — loading spinners, useEffect chains, client-side caching layers, and API route scaffolding — is replaced by direct data access during render.
Direct Database Access
Server Components can import database clients and query directly:
import db from './database'
async function UserProfile({ userId }) {
const user = await db.users.findById(userId)
const posts = await db.posts.findByAuthor(userId)
return (
<div>
<h1>{user.name}</h1>
</div>
)
}
No API route. No fetch call. No loading state. The data flows directly from the database into the rendered output.
Async Components
Server Components can be async functions. This is a new capability — Client Components cannot be async. The await keyword works at the component body level, making sequential data fetching natural:
async function Dashboard() {
const user = await getCurrentUser()
const notifications = await getNotifications(user.id)
const stats = await getDashboardStats(user.id)
return (
<div>
</div>
)
}
Eliminating Client-Server Waterfalls
In client-heavy applications, a common antipattern is the waterfall: a parent component fetches data, renders, and triggers a child component’s useEffect, which then fetches its own data. Each fetch depends on the previous render completing. With Server Components, all data is available before rendering begins, eliminating these cascading delays.
The React documentation provides a clear example: a Note component fetches a note, then an Author component needs to fetch the author. In the client model, this creates a waterfall. In the Server Component model, both fetches occur during render on the server, where latency to the data source is negligible compared to client-server round trips.
Parallel Fetching with Suspense
For independent data sources, Server Components compose naturally with React Suspense to enable parallel streaming:
async function Page() {
return (
<div>
<Suspense fallback={}>
<Header />
<Suspense fallback={}>
<Suspense fallback={}>
</div>
)
}
Each Suspense boundary streams independently. The header renders and streams to the client while the main content and sidebar are still fetching data. The user sees progressive content loading without any client-side coordination.
Streaming and Progressive Rendering
Server Components integrate with React’s streaming architecture to deliver content progressively. Rather than waiting for the entire page to render before sending anything to the client, the server streams HTML in chunks as each component finishes rendering.
Streaming provides three distinct performance benefits:
- Faster First Contentful Paint (FCP) — The browser receives and renders initial content immediately, without waiting for slow data fetches.
- Reduced Time to Interactive (TTI) — Client components hydrate progressively as their JavaScript arrives, rather than all at once after a monolithic bundle downloads.
- Perceived performance improvement — Users see a skeleton or loading state within milliseconds, and content fills in as it becomes available. This dramatically improves perceived responsiveness.
In Next.js App Router, streaming is the default behavior. Route segments wrapped in Suspense boundaries automatically stream. The framework handles the transport layer — you declare what can stream, and React handles the rest.
The streaming model also enables a powerful pattern: rendering expensive or slow components in isolation without blocking faster components. A page with a fast header, a moderate article body, and a slow recommendation engine can stream each section independently, giving the user a progressively richer experience.
Composition Patterns
The most important skill in the RSC era is understanding how Server and Client Components compose. The rules are strict but logical.
The Interleaving Pattern
Server Components can import and render Client Components. Client Components cannot import Server Components directly, but they can receive them as children or props. This creates a powerful composition model:
// Server Component
import LikeButton from './LikeButton'
async function ArticlePage({ id }) {
const article = await getArticle(id)
return (
<article>
<h1>{article.title}</h1>
<div>{article.body}</div>
</article>
)
}
// Client Component
'use client'
import { useState } from 'react'
export default function LikeButton({ articleId, initialCount }) {
const <!--ls-md-69b6de102db6e-SHORTCODE-0--> = useState(initialCount)
return <button> handleLike(articleId, setLikes)}>{likes} likes</button>
}
The article content is rendered on the server. The interactive like button is a Client Component that receives serialized props. The client JavaScript is minimal — only the button’s interactivity logic ships to the browser.
The Children Pattern
A Client Component can accept Server Component output as children. This is the recommended pattern for interactive wrappers around server-rendered content:
// Client Component wrapper
'use client'
import { useState } from 'react'
export default function Collapsible({ children, title }) {
const <!--ls-md-69b6de102db6e-SHORTCODE-1--> = useState(false)
return (
<div>
<button> setIsOpen(!isOpen)}>{title}</button>
{isOpen && children}
</div>
)
}
// Server Component using the wrapper
import Collapsible from './Collapsible'
async function FAQ() {
const questions = await getFAQData()
return (
<div>
{questions.map(q => (
<p>{q.answer}</p> {/ Server-rendered content passed as children /}
))}
</div>
)
}
The Collapsible component manages interactive state (open/closed) on the client, but the content it wraps is rendered entirely on the server. No content JavaScript reaches the client.
The Props Serialization Pattern
Server Components can pass serializable data as props to Client Components. The data is serialized into the RSC payload and deserialized on the client. Props must be serializable — you cannot pass functions, class instances, or complex objects with methods. Strings, numbers, booleans, arrays of primitives, and plain objects work reliably.
Common Mistakes
Mistake 1: Adding ‘use client’ at the Layout Level
The most damaging mistake is marking the root layout or a high-level layout component as a Client Component. Because "use client" is a boundary, this forces every child component — including purely presentational ones — into the client bundle. The result is a massive client JavaScript payload that defeats the entire purpose of Server Components.
Place "use client" only at the leaves of your component tree, where interactivity is actually required.
Mistake 2: Fetching Data in useEffect Instead of Server Components
Developers accustomed to the client-side paradigm continue fetching data in useEffect hooks even when the component could be a Server Component. If your component fetches data and does not need to re-fetch in response to user interaction, it should be a Server Component that fetches during render.
// WRONG — Client Component fetching in useEffect
'use client'
function ProductList() {
const <!--ls-md-69b6de102db6e-SHORTCODE-2--> = useState([])
useEffect(() => {
fetch('/api/products').then(r => r.json()).then(setProducts)
}, [])
return <div>{/ render products /}</div>
}
// RIGHT — Server Component fetching during render
async function ProductList() {
const products = await db.products.findAll()
return <div>{/ render products /}</div>
}
Mistake 3: Importing Server Components into Client Components
This is a structural error. A Client Component cannot import and render a Server Component directly, because the Server Component’s code does not exist on the client. The error manifests as a runtime failure. The fix is to restructure the composition — either pass the Server Component as a child prop, or elevate the Client Component to be a child of the Server Component instead.
Mistake 4: Passing Non-Serializable Props
Functions, class instances, React context providers, and other non-serializable values cannot be passed from Server Components to Client Components. The RSC payload only supports serializable data. If you need to share complex logic, extract the serializable data in the Server Component and pass only the data to the Client Component, which then applies its own logic.
Mistake 5: Overusing Client Components for Styling
Components that apply conditional CSS classes based on props do not need to be Client Components. Conditional class application is a pure render-time operation that Server Components handle perfectly. Only the CSS class logic that responds to user interaction (hover, click, scroll position) requires a Client Component.
Migration Strategy from Client-Heavy Applications
Migrating an existing client-heavy React application to Server Components is an incremental process. You do not need to rewrite everything at once.
Step 1: Upgrade and Adopt the App Router
If you are using Next.js Pages Router, begin by adopting the App Router. You can run both routers simultaneously during migration. New routes use the App Router with Server Components by default. Existing routes continue using the Pages Router until they are migrated.
Step 2: Audit Your Component Tree
Map your component hierarchy and categorize every component:
- Server-candidate components — Components that render data, apply conditional styling, or compose other components without managing state. These are migration targets.
- Client-required components — Components using useState, useEffect, event handlers, or browser APIs. These need the “use client” directive.
- Ambiguous components — Components that could be either depending on how they are used. These need architectural decisions.
Step 3: Move Data Fetching to Server Components
Start with your data-fetching components. Convert useEffect-based data fetching to Server Component async functions. This single change eliminates loading states, client-server waterfalls, and the need for client-side caching solutions like React Query or SWR for server data.
Step 4: Push the Client Boundary Downward
For each component currently marked with "use client", evaluate whether the entire component needs to be a Client Component or whether you can extract the interactive parts into smaller Client Components while keeping the surrounding structure as a Server Component.
Step 5: Refactor State Management
Client-side global state management (Redux, Zustand, Jotai) often stores data that originates from the server. With Server Components, this data can live entirely on the server and flow through props. Evaluate which pieces of global state actually represent client-side UI state (modals, theme preferences, form drafts) and which are server data in disguise. Server data belongs in Server Components.
Step 6: Test Bundle Size Reduction
After each migration phase, measure your client JavaScript bundle size. Tools like @next/bundle-analyzer reveal which dependencies remain in the client bundle and why. The goal is steady reduction with each migration milestone.
Next.js App Router Integration
Next.js App Router is the reference implementation of React Server Components. Understanding its conventions is essential for anyone working with RSCs.
Default Server Components
In the App Router, every component is a Server Component by default. There is no directive needed. A page component, a layout component, or any component file without "use client" renders on the server. This inverts the historical assumption that every React component is client-side.
Route Segments and Streaming
Each route segment (layout, page, loading, error) in the App Router creates a natural Suspense boundary. The loading.js file provides an automatic fallback while the page component’s data fetches. The error.js file handles errors at that segment level. This structure makes streaming the default experience.
Server Functions
The "use server" directive marks Server Functions — functions that can be called from Client Components but execute on the server. These are distinct from Server Components. A Server Function is a callable endpoint; a Server Component is a render function. Do not confuse the two directives:
- “use client” — This file contains Client Components (or exports that will be used by Client Components).
- “use server” — This file contains Server Functions (async functions callable from the client).
Parallel Route Segments
Next.js supports parallel routes using named slots, allowing multiple page components to render simultaneously in different parts of the layout. Each slot is an independent rendering boundary that can stream independently. This pattern is powerful for dashboards and complex layouts where different sections fetch different data sources.
Route Handlers for Non-UI Endpoints
When you need traditional API endpoints (webhooks, third-party integrations, file uploads), App Router provides route.js files. These are not Server Components — they are request handlers. Use them for cases where Server Components are not the right abstraction.
Decision Framework
Use this framework to determine the correct component type for any given component:
Choose Server Components when:
- The component renders data that does not change based on user interaction.
- You need to access databases, file systems, or internal APIs directly.
- You want to keep API keys and secrets server-side.
- The component uses heavy libraries that are unnecessary on the client.
- You are building static or semi-static content (blog posts, documentation, product descriptions).
- You want to reduce client JavaScript bundle size.
- The component is primarily a layout, wrapper, or structural element.
Choose Client Components when:
- The component manages interactive state (form inputs, toggles, counters, modals).
- You need event handlers (onClick, onChange, onSubmit, onKeyDown).
- The component uses useEffect for lifecycle-dependent logic.
- You require browser-only APIs (localStorage, window, IntersectionObserver, matchMedia).
- The component integrates with third-party libraries that depend on the DOM or browser environment.
- You are building real-time features that require WebSocket connections or server-sent events managed on the client.
- The component uses custom hooks that depend on client-side React primitives.
The default answer is Server Component. Only move to a Client Component when the component demonstrably requires client-side capabilities. This is not a preference — it is a performance discipline.
The architectural principle: compose Server and Client Components by rendering Client Components inside Server Components, never the reverse. Let the server be the foundation and the client be the interactive layer on top. Push interactivity to the edges of your component tree. Keep the trunk on the server.
This is the architecture that React is building toward. The developers who internalize this distinction now will build faster, leaner, and more maintainable applications than those still fighting against the model with client-heavy architectures from 2020.
Frequently Asked Questions
Are React Server Components the same as Server-Side Rendering?
No. Server-Side Rendering (SSR) converts React components to HTML on the server for the initial page load, but the component JavaScript still ships to the client for hydration. Server Components never send their JavaScript to the client at all. SSR and RSCs can work together — Server Components are rendered on the server, their output can be SSR’d to HTML for the initial paint, and only the Client Components in the tree hydrate on the client.
Can I use React Server Components without Next.js?
Technically yes, but practically no for most teams. Next.js App Router is the only production-grade framework with full RSC support as of 2026. The underlying RSC APIs are stable in React 19, but the bundler and runtime integration required to support them is complex. Other frameworks like Remix, Waku, and experimental tooling are building RSC support, but Next.js remains the reference implementation and the most mature option.
Do Server Components re-render on navigation?
On subsequent client-side navigations, Server Components are re-fetched from the server and re-rendered. The RSC payload for the new route is delivered, and React reconciles the tree on the client. Next.js prefetches and caches these payloads for instant navigation performance. Server Components do not persist across navigations the way Client Components do.
Can I use hooks in Server Components?
No. Hooks like useState, useEffect, useRef, useContext, useMemo, and useCallback are Client Component APIs. Server Components cannot use them because they do not execute in a browser environment and do not have a lifecycle in the traditional sense. Server Components can use server-only hooks provided by frameworks, but not React’s client-side hook API.
How do I handle forms with Server Components?
Server Components can render form markup, but the form submission handler must be a Client Component or a Server Function. The recommended pattern is to render the form in a Server Component and use a Server Function (marked with "use server") as the form action. This keeps the form HTML on the server while the submission logic executes in a server environment without shipping form-handling JavaScript to the client.
What happens to my existing React Router or state management library?
React Router v6+ works with Server Components when used within a framework that supports them (like Remix or a custom RSC setup). For state management, evaluate which state is truly client-side (UI state like modals, theme, form drafts) versus server data. Server data no longer needs a client-side store — it flows from Server Components through props. Libraries like Zustand, Jotai, and Redux remain valuable for managing genuine client-side state, but their role shrinks significantly in an RSC architecture.
Is it safe to pass sensitive data from Server Components to Client Components?
No. Any data passed as props from a Server Component to a Client Component is serialized into the RSC payload and sent to the browser. This means the user can inspect it. Only pass data that is safe to expose to the client. Sensitive operations — database queries with access control, API calls with secret tokens, permission checks — should happen entirely within Server Components, and only the safe, filtered result should be passed to Client Components.