Resourcesโ€บReact Patternsโ€บReact Server Components Explained: No More Client-Side Bloat
โš›๏ธReact Patternsโ€” React Server Components Explained: No More Client-Side Bloatโฑ 8 min

React Server Components Explained: No More Client-Side Bloat

React Server Components fundamentally change how React apps are built. Here's a clear explanation of what they are, how they work, and when to use them.

๐Ÿ“…January 16, 2026โœTechTwitter.ioreactserver-componentsnextjsperformance

The Problem Server Components Solve

Traditional React: every component ships JavaScript to the browser. The browser downloads it, parses it, runs it to fetch data, then renders. For a data-heavy dashboard, you might ship 500KB of JavaScript just to display database records.

React Server Components: components render on the server. The HTML arrives in the browser. Zero JavaScript for that component shipped to the client.


Server vs Client Components

Server Component (default in Next.js App Router):

  • Renders on the server
  • Can directly access databases, file systems, secrets
  • Zero JavaScript shipped to the browser
  • Cannot use state (useState), effects (useEffect), or browser APIs

Client Component (opt-in with 'use client'):

  • Renders on the client (and optionally server-side pre-rendered)
  • Full React features: state, effects, event handlers
  • JavaScript shipped to browser
// server-component.tsx โ€” no 'use client', runs only on server
import { db } from '@/lib/db'

export default async function UserList() {
  const users = await db.users.findMany() // Direct DB access โ€” no API needed!
  return (
    <ul>
      {users.map(u => <li key={u.id}>{u.name}</li>)}
    </ul>
  )
}
// client-component.tsx โ€” 'use client' at the top
'use client'
import { useState } from 'react'

export default function Counter() {
  const [count, setCount] = useState(0)
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}

The Composition Model

You can nest Client Components inside Server Components, but not Server Components inside Client Components (directly):

// โœ… Server Component rendering a Client Component
// app/page.tsx (Server Component)
import { Counter } from './Counter'  // Client Component

export default async function Page() {
  const data = await fetchData()  // Server-side fetch
  return (
    <div>
      <h1>{data.title}</h1>
      <Counter />  {/* Client Component โ€” fine here */}
    </div>
  )
}
// โœ… Pass Server-rendered content as children to Client Components
// ClientWrapper.tsx
'use client'
export function ClientWrapper({ children }) {
  const [open, setOpen] = useState(false)
  return (
    <div>
      <button onClick={() => setOpen(!open)}>Toggle</button>
      {open && children}  {/* children can be Server Component output */}
    </div>
  )
}

// app/page.tsx (Server Component)
import { ClientWrapper } from './ClientWrapper'
import { ServerContent } from './ServerContent'

export default function Page() {
  return (
    <ClientWrapper>
      <ServerContent />  {/* Server Component passed as children */}
    </ClientWrapper>
  )
}

When to Use Server Components

Use Server Components for:

  • Fetching data from databases, APIs, file system
  • Components that display data without user interaction
  • Hiding sensitive operations (API keys, database queries) from the client
  • Large dependencies that don't need to run in the browser

Use Client Components for:

  • useState, useReducer, useEffect
  • Event handlers (onClick, onChange, etc.)
  • Browser-only APIs (window, localStorage, navigator)
  • Custom hooks that use the above

Async Components

Server Components can be async โ€” use await directly in the component body:

// No useEffect, no useState, no loading state needed
export default async function ProductPage({ params }) {
  const [product, reviews] = await Promise.all([
    db.products.findUnique({ where: { id: params.id } }),
    db.reviews.findMany({ where: { productId: params.id } }),
  ])

  return (
    <div>
      <h1>{product.name}</h1>
      <ReviewList reviews={reviews} />
    </div>
  )
}

No more useEffect(() => { fetchData() }, []). The data is already there when the component renders.


Streaming with Suspense

Slow data fetches don't block the entire page with Server Components + Suspense:

import { Suspense } from 'react'

export default function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>
      {/* Fast โ€” renders immediately */}
      <QuickStats />

      {/* Slow query โ€” streams in when ready */}
      <Suspense fallback={<ReportSkeleton />}>
        <SlowReport />
      </Suspense>
    </div>
  )
}

The page HTML is sent to the browser progressively. Users see the fast parts immediately; the slow parts arrive as they complete.


Key Takeaways

  • Server Components run only on the server โ€” no JavaScript shipped to the browser
  • Use 'use client' to opt into client-side features (state, effects, events)
  • Server Components can be async โ€” fetch data directly with await
  • Nest Client Components inside Server Components freely; pass Server content as children the other way
  • Use Suspense to stream slow parts โ€” fast parts render immediately
  • Default to Server Components; opt into Client only when you need interactivity