Next.js

React Server Components: What They Are and When to Use Them

Svet Halachev Dec 20, 2025 5 min read
React Server Components: What They Are and When to Use Them

Server Components fundamentally change how React applications work. They're not just a performance optimization—they're a different mental model for building UIs.

Let me explain what's actually happening and how to think about them.

The Core Idea

Traditional React components run in the browser. Your JavaScript bundle ships to the client, React executes it, and components render.

Server Components flip this: they run on the server, render to HTML (plus a special format React understands), and the result gets sent to the client. The component code never reaches the browser.

// This component runs on the server
// Its code is never sent to the browser
async function RecentPosts() {
  const posts = await db.post.findMany({
    take: 5,
    orderBy: { createdAt: 'desc' },
  })

  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

Notice the async. Server Components can be async functions—they can await data directly. No useEffect, no loading states for initial render. The component waits for its data, renders, and sends the result.

Server vs. Client Components

In Next.js App Router, every component is a Server Component by default. To make a Client Component, you add 'use client' at the top:

'use client'

import { useState } from 'react'

export function Counter() {
  const [count, setCount] = useState(0)

  return (
    <button onClick={() => setCount(count + 1)}>
      Count: {count}
    </button>
  )
}

Here's what each type can do:

Server Components can:

  • Access backend resources directly (database, file system)

  • Use async/await for data fetching

  • Keep sensitive logic and API keys on the server

  • Reduce client bundle size (their code isn't shipped)

Server Components cannot:

  • Use hooks like useState, useEffect, useContext

  • Access browser APIs (window, localStorage, etc.)

  • Use event handlers (onClick, onChange, etc.)

  • Use class components

Client Components can:

  • Use all React hooks

  • Handle user interactions

  • Access browser APIs

  • Maintain client-side state

The Mental Model

Think of your component tree as having a "client boundary." Once you mark a component with 'use client', that component AND all its children become Client Components—even if they don't have the directive.

// Server Component (default)
export default function Page() {
  return (
    <div>
      <Header />           {/* Server Component */}
      <ArticleContent />   {/* Server Component */}
      <CommentSection />   {/* Client Component */}
    </div>
  )
}

// Header.tsx - no directive needed, it's a Server Component
export function Header() {
  return <header>...</header>
}

// CommentSection.tsx - needs interactivity
'use client'
export function CommentSection() {
  const [comments, setComments] = useState([])
  // This component and its children are Client Components
  return <div>...</div>
}

Practical Patterns

Pattern 1: Push Client Components Down

Make the smallest possible components Client Components:

// Bad: Entire page is client-side
'use client'
export default function ProductPage() {
  const [quantity, setQuantity] = useState(1)

  return (
    <div>
      <ProductInfo />          {/* Could be server-rendered! */}
      <ProductReviews />       {/* Could be server-rendered! */}
      <QuantitySelector        {/* Only this needs client */}
        quantity={quantity}
        onChange={setQuantity}
      />
    </div>
  )
}

// Good: Only interactive parts are client-side
export default async function ProductPage({ params }) {
  const product = await getProduct(params.id)

  return (
    <div>
      <ProductInfo product={product} />
      <ProductReviews productId={product.id} />
      <AddToCartButton product={product} />  {/* Client Component */}
    </div>
  )
}

Pattern 2: Pass Server Data to Client Components

Server Components can fetch data and pass it as props:

// Server Component
export default async function Dashboard() {
  const initialData = await fetchDashboardData()

  return (
    <DashboardClient initialData={initialData} />
  )
}

// Client Component
'use client'
export function DashboardClient({ initialData }) {
  const [data, setData] = useState(initialData)

  // Now you have server-fetched data as initial state
  // and can update it client-side
}

Pattern 3: Composition for Context Providers

You can't use context in Server Components, but you can wrap them:

// app/providers.tsx
'use client'

import { ThemeProvider } from 'next-themes'

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <ThemeProvider attribute="class">
      {children}
    </ThemeProvider>
  )
}

// app/layout.tsx (Server Component)
import { Providers } from './providers'

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <Providers>
          {children}  {/* Server Components work inside! */}
        </Providers>
      </body>
    </html>
  )
}

The children passed through a Client Component can still be Server Components.

Pattern 4: Streaming with Suspense

Server Components work beautifully with Suspense for streaming:

import { Suspense } from 'react'

export default function Page() {
  return (
    <div>
      <Header />  {/* Renders immediately */}

      <Suspense fallback={<PostsSkeleton />}>
        <RecentPosts />  {/* Streams in when ready */}
      </Suspense>

      <Suspense fallback={<CommentsSkeleton />}>
        <Comments />  {/* Streams in independently */}
      </Suspense>
    </div>
  )
}

The page shell shows immediately, then each Suspense boundary fills in as its data arrives. Users see content faster, and slow data sources don't block the entire page.

When to Use Which

Use Server Components when:

  • Fetching data

  • Accessing backend services

  • Rendering static or mostly-static content

  • The component doesn't need interactivity

  • You want to keep code/dependencies off the client

Use Client Components when:

  • You need useState, useEffect, or other hooks

  • You need event handlers

  • You're using browser APIs

  • You need React context (though providers can wrap Server Components)

  • You're using client-side libraries (many UI libraries require client)

Common Mistakes

Mistake 1: Adding 'use client' to everything

Don't do this out of habit. Every component you make a Client Component ships to the browser and adds to your bundle.

Mistake 2: Trying to use hooks in Server Components

This just won't work. If you need state, that component needs to be a Client Component.

Mistake 3: Importing Server Components into Client Components

This doesn't work directly. Instead, pass them as children or other React node props.

The Bigger Picture

Server Components aren't just about performance—they simplify your mental model. Data fetching happens where the data lives (the server). Interactivity happens where the user is (the browser). Each component runs in the environment that makes sense for what it does.

It takes some adjustment, but once it clicks, you'll find yourself writing simpler code that performs better by default.

Related Articles