ReactReact Server Components Overview

React Server Components Overview

React Server Components (RSC) represent the most significant architectural shift in React since Hooks. Introduced as a stable feature in React 19 and available today via the Next.js App Router, RSC lets certain components run exclusively on the server — they never ship their JavaScript to the browser, can query databases directly, and compose with client components through a well-defined boundary.

Before RSC, React always sent component code to the browser. Even for a component that just reads a database and renders a table, the JavaScript needed to hydrate that component was bundled and downloaded. RSC breaks that assumption: a server component executes on the server, sends HTML (and a serialised React tree) to the client, and contributes zero bytes of JavaScript to the browser bundle.

The Mental Model

Think of your component tree as having two zones separated by a boundary:

  • Server zone — components that run at request time (or build time) on the server. They can await database queries, read files, access environment variables, and call internal APIs. They are async by default.

  • Client zone — components that run in the browser (and are also server-rendered for the initial HTML). They can use state, effects, event handlers, and browser APIs. They must be marked with 'use client'.

The boundary between zones is a one-way membrane: server components can import and render client components, but client components cannot import server components (they can, however, receive them as the children prop — which is how you "pass through" server content into client trees).

Your First Server Component

In the Next.js App Router every component is a server component by default. You don't need any special decorator — just write an async function and await your data:

TSX
// app/posts/page.tsx — this is a Server Component
// No 'use client' directive → runs only on the server

interface Post {
  id: number
  title: string
  body: string
}

async function getPosts(): Promise<Post[]> {
  // Direct database access, or any server-side call
  const res = await fetch('https://jsonplaceholder.typicode.com/posts?_limit=5', {
    next: { revalidate: 3600 }, // ISR: revalidate every hour
  })
  return res.json()
}

export default async function PostsPage() {
  const posts = await getPosts() // ✅ top-level await — no useEffect needed

  return (
    <main>
      <h1>Latest Posts</h1>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>
            <h2>{post.title}</h2>
            <p>{post.body}</p>
          </li>
        ))}
      </ul>
    </main>
  )
}

Notice what is absent: no useState, no useEffect, no loading state management. The component awaits data before rendering — React handles the rest.

The `'use client'` Directive

To opt a component into the client zone, add 'use client' at the top of the file. This marks the file as a client boundary — React will bundle everything imported from this file onward into the browser bundle.

TSX
'use client'
// Everything in this file (and its imports) goes to the browser bundle

import { useState } from 'react'

interface LikeButtonProps {
  initialCount: number
}

export function LikeButton({ initialCount }: LikeButtonProps) {
  const [count, setCount] = useState(initialCount)

  return (
    <button onClick={() => setCount((c) => c + 1)}>
      ❤️ {count}
    </button>
  )
}

A server component can then import and render LikeButton — the server component fetches the initial count from the database and passes it as a prop. The client component takes over interactivity in the browser:

TSX
// app/posts/[id]/page.tsx — Server Component
import { LikeButton } from '@/components/LikeButton' // client component
import { db } from '@/lib/db'

export default async function PostPage({ params }: { params: { id: string } }) {
  const post = await db.post.findUnique({ where: { id: params.id } })
  const likes = await db.like.count({ where: { postId: params.id } })

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.body}</p>
      {/* Server renders initialCount; client handles clicks */}
      <LikeButton initialCount={likes} />
    </article>
  )
}
RSC vs SSR vs SSG

Concept

Where runs

When

JS in bundle?

SSG (Static Site Generation)

Server

Build time only

Yes (hydration)

SSR (Server-Side Rendering)

Server

Every request

Yes (hydration)

React Server Components

Server

Request or build

No

Client Components

Browser (+ server for initial HTML)

Browser runtime

Yes

SSR and SSG generate HTML on the server but still send all the React component JavaScript to the browser for hydration (making the HTML interactive). RSC is different: because a server component has no interactivity, there is nothing to hydrate — no JavaScript is required in the browser for that component.

Streaming with Suspense

RSC pairs naturally with React's Suspense to stream HTML in chunks. Wrap a slow server component in Suspense and React sends the shell immediately, then streams the resolved content as it becomes ready:

TSX
import { Suspense } from 'react'
import { ProductList } from './ProductList'   // slow DB query
import { RecommendedItems } from './Recommended' // even slower ML call

export default function ShopPage() {
  return (
    <main>
      <h1>Our Store</h1>

      {/* Streams in when the DB query resolves */}
      <Suspense fallback={<p>Loading products…</p>}>
        <ProductList />
      </Suspense>

      {/* Streams in independently — doesn't block ProductList */}
      <Suspense fallback={<p>Loading recommendations…</p>}>
        <RecommendedItems />
      </Suspense>
    </main>
  )
}

Without streaming, the page would wait for the slowest query before sending a single byte. With streaming, users see the shell and fast sections immediately — a dramatic improvement in Time to First Byte (TTFB) and Largest Contentful Paint (LCP).

What Server Components Cannot Do
  • Use useState, useEffect, useReducer, or any hook that manages browser state

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

  • Access browser-only APIs (window, document, localStorage)

  • Use React Context directly (though you can read values from a context set up in a client wrapper)

  • Be imported by a client component (pass as children instead)

Warning
If you try to use `useState` in a server component, Next.js will throw a build-time error. The `'use client'` directive is not optional when you need hooks or event handlers.
Passing Server Data into Client Components

The children pattern is the idiomatic way to get server-rendered content inside a client component without crossing the boundary illegally:

TSX
// ClientShell.tsx — client component for interactive wrapper
'use client'
import { useState } from 'react'

export function ClientShell({ children }: { children: React.ReactNode }) {
  const [open, setOpen] = useState(true)

  return (
    <div>
      <button onClick={() => setOpen((o) => !o)}>Toggle</button>
      {open && children}
    </div>
  )
}

// page.tsx — server component
import { ClientShell } from './ClientShell'
import { SlowServerContent } from './SlowServerContent' // server component

export default function Page() {
  return (
    <ClientShell>
      {/* SlowServerContent renders on the server, result passed as children */}
      <SlowServerContent />
    </ClientShell>
  )
}
Why RSC Is the Future of React Data Fetching
  • Zero waterfall by default — each server component fetches its own data in parallel, unlike nested useEffect chains

  • No client bundle cost — libraries used only in server components (ORMs, markdown parsers, heavy utilities) don't ship to the browser

  • Type-safe data flow — pass typed data from the database directly to JSX without a REST layer

  • Simplified mental model for data — no need to manage loading/error state for initial page data; use Suspense + error boundaries declaratively

  • Better security — database credentials, API keys, and internal service calls never leave the server

Current availability
React Server Components are stable in Next.js 13.4+ (App Router). Remix, Gatsby, and other frameworks are working on their own RSC implementations. For non-framework projects, RSC requires a custom server setup that is non-trivial — use a framework.
Quick Reference: Server vs Client

Feature

Server Component

Client Component

async/await at top level

✅ Yes

❌ No (use useEffect)

Database / file system access

✅ Yes

❌ No

useState / useReducer

❌ No

✅ Yes

useEffect

❌ No

✅ Yes

Event handlers (onClick etc.)

❌ No

✅ Yes

Browser APIs

❌ No

✅ Yes

React Context

❌ (read only via client wrapper)

✅ Yes

JavaScript in browser bundle

❌ Zero

✅ Yes

Note
The Next.js App Router documentation is the authoritative source for RSC behaviour. As React 19 matures, expect the RSC specification to stabilise further across frameworks beyond Next.js.