Published on

React Server Components in Next.js - Complete Guide

Authors

Introduction

React Server Components (RSC) are the biggest architectural shift in React's history. They run entirely on the server, can fetch data directly without useEffect, and send zero JavaScript to the client.

In Next.js, Server Components are the default — every component is a Server Component unless you add 'use client'.

Server vs Client Components

// SERVER COMPONENT (default in Next.js App Router)
// ✅ Can fetch data directly (no useEffect)
// ✅ Can access backend resources (DB, filesystem)
// ✅ Sends ZERO JavaScript to the client
// ❌ Cannot use useState, useEffect, event handlers
// ❌ Cannot use browser APIs

async function ProductList() {
  // Direct async/await — no useEffect needed!
  const products = await db.product.findAll()

  return (
    <ul>
      {products.map(p => <li key={p.id}>{p.name}</li>)}
    </ul>
  )
}
// CLIENT COMPONENT — add 'use client' at the top
'use client'

// ✅ Can use useState, useEffect, event handlers
// ✅ Can use browser APIs (window, localStorage)
// ❌ Cannot be async (no direct DB access)
// ❌ All code is bundled and sent to the browser

import { useState } from 'react'

function AddToCartButton({ productId }: { productId: number }) {
  const [added, setAdded] = useState(false)

  return (
    <button onClick={() => setAdded(true)}>
      {added ? '✓ Added' : 'Add to Cart'}
    </button>
  )
}

The Mental Model: Two Environments

SERVER                              CLIENT (Browser)
─────────────────────────────────   ─────────────────
ProductPage (Server)                CartButton (Client)
  ↓ fetches from DB                   ↓ handles click
  ↓ renders HTML                      ↓ updates state
  ↓ sends to browser                  ↓ re-renders

Server Components fetch and render. Client Components handle interactivity.

Data Fetching in Server Components

No more useEffect + useState for data fetching:

app/users/page.tsx
// ✅ Clean, direct data fetching
async function UsersPage() {
  const users = await prisma.user.findMany({
    orderBy: { createdAt: 'desc' },
    take: 20,
  })

  return (
    <main>
      <h1>Users</h1>
      {users.map(user => (
        <UserCard key={user.id} user={user} />
      ))}
    </main>
  )
}

Compare this to the equivalent with the Pages Router:

// Old way — boilerplate
export async function getServerSideProps() {
  const users = await prisma.user.findMany()
  return { props: { users } }
}

export default function UsersPage({ users }) {
  return (/* render */)
}

Server Components eliminate the boilerplate entirely.

Composing Server and Client Components

The key rule: Server Components can import Client Components, but not vice versa (for Server logic).

app/products/[id]/page.tsx
// Server Component — fetches data
async function ProductPage({ params }: { params: { id: string } }) {
  const product = await getProduct(params.id)
  const reviews = await getReviews(params.id)

  return (
    <div>
      {/* Static product info — Server Component */}
      <ProductInfo product={product} />

      {/* Interactive button — Client Component */}
      <AddToCartButton productId={product.id} price={product.price} />

      {/* Reviews with interactive sort — Client Component */}
      <ReviewList reviews={reviews} />
    </div>
  )
}
components/AddToCartButton.tsx
'use client'  // This component is interactive

import { useState } from 'react'
import { addToCart } from '@/app/actions'  // Server Action!

export function AddToCartButton({
  productId,
  price
}: {
  productId: number
  price: number
}) {
  const [loading, setLoading] = useState(false)

  async function handleClick() {
    setLoading(true)
    await addToCart(productId)
    setLoading(false)
  }

  return (
    <button onClick={handleClick} disabled={loading}>
      {loading ? 'Adding...' : `Add to Cart — $${price}`}
    </button>
  )
}

Parallel Data Fetching

Fetch multiple resources simultaneously in Server Components:

app/dashboard/page.tsx
async function DashboardPage() {
  // ✅ Parallel fetching — all run simultaneously!
  const [user, stats, recentOrders] = await Promise.all([
    getUser(),
    getDashboardStats(),
    getRecentOrders(),
  ])

  return (
    <div>
      <WelcomeBanner user={user} />
      <StatsGrid stats={stats} />
      <OrdersList orders={recentOrders} />
    </div>
  )
}

Streaming with Suspense

Stream different parts of your UI as they become ready:

app/dashboard/page.tsx
import { Suspense } from 'react'

export default function DashboardPage() {
  return (
    <main>
      {/* Renders immediately — no data needed */}
      <DashboardHeader />

      {/* Streams in when ready */}
      <Suspense fallback={<StatsSkeleton />}>
        <StatsSection />
      </Suspense>

      {/* Streams in independently */}
      <Suspense fallback={<OrdersSkeleton />}>
        <RecentOrders />
      </Suspense>
    </main>
  )
}

async function StatsSection() {
  const stats = await getStats()  // Might be slow
  return <StatsGrid stats={stats} />
}

async function RecentOrders() {
  const orders = await getOrders()  // Also might be slow
  return <OrdersList orders={orders} />
}

The header renders instantly. Stats and orders stream in independently as each finishes — no waterfall!

Passing Server Data to Client Components

// Server Component — has access to DB
async function UserSettingsPage() {
  const user = await getUser()  // Server-side only

  // Pass data as props to Client Component
  return <SettingsForm initialData={user} />
}
'use client'

// Client Component — receives data as props, handles interaction
export function SettingsForm({ initialData }) {
  const [name, setName] = useState(initialData.name)

  return (
    <form>
      <input value={name} onChange={e => setName(e.target.value)} />
    </form>
  )
}

When to Use Each

Use CaseComponent Type
Fetching data from DBServer Component
Accessing filesystemServer Component
Using secret keys/envServer Component
Rendering large listsServer Component
useState / useEffectClient Component
Event listeners (onClick)Client Component
Browser APIs (localStorage)Client Component
AnimationsClient Component

Default rule: Start with Server Components. Add 'use client' only when you need interactivity.

Conclusion

React Server Components represent a paradigm shift — moving data fetching back to the server while keeping interactivity in the browser, exactly where each belongs. The result is apps that are faster (less JS shipped), simpler (no useEffect for data fetching), and more secure (secrets stay on the server). Master Server Components and you'll unlock the full power of modern Next.js.