⚑

Where Code Runs

The server/client boundary is the foundational mental model for React Server Components

In React's new model, every component has a designated runtime β€” either the server or the browser. Server Components run once at request time with zero client bundle cost, while Client Components hydrate in the browser and power all interactivity. The boundary between them is called the serialization boundary, and understanding it unlocks the full power of the RSC model.

use client directiveSerialization BoundaryZero Bundle CostServer-First RenderingAsync ComponentsClient-Side StateBrowser APIsComponent Composition

Runtime Environments

Each component type has a distinct set of capabilities and constraints.

πŸ–₯
Server Components
default
βœ“

Direct Data Access

Databases, file systems, and backend services β€” no API layer

βœ“

Secrets Stay Secret

API keys and tokens never leave the server environment

βœ“

Zero Client JS

Component code is never shipped to the browser

βœ“

Async by Default

Await data directly in the component body with no hooks

Not available on server:

  • βœ— useState, useEffect, useContext
  • βœ— Browser APIs (window, document)
  • βœ— Event handlers (onClick, onChange)
app/pokemon/page.tsx
Server
// No directive β€” Server Component by default
async function PokemonPage() {
  // βœ“ Direct DB β€” zero extra API layer
  const  list = await db.pokemon.findMany({
    orderBy: { name: "asc" },
  );

  // βœ“ Secrets never reach the browser
  const  key = process.env.STRIPE_KEY;

  return <PokemonGrid list={list} />;
}
🌐
Client Components
"use client"
βœ“

Interactivity

onClick, onChange, form submissions, and all event handlers

βœ“

Local State

useState, useReducer, and useContext for reactive UI

βœ“

Browser APIs

window, localStorage, geolocation, and Web APIs

βœ“

Lifecycle Hooks

useEffect for side effects, timers, and subscriptions

Not available on client:

  • βœ— Direct database / file-system access
  • βœ— Backend-only Node.js libraries
  • βœ— Environment secrets (exposed in bundle)
components/FavoriteButton.tsx
Client
"use client"  // ← opts the file into the client bundle
import { useState } from "react"

function FavoriteButton() {
  // βœ“ Client state β€” not possible on server
  const  [liked, setLiked] = useState(false);

  return (
    <button onClick={()=> setLiked(!liked)}
      className{liked ? "text-red-500" : "text-gray-400"}
    > {liked ? "❀️" : "🀍"} </button>
  );
}

Component Composition

Server Components can render Client Components β€” the reverse is not allowed at the module level.

Live Component Tree
ServerClientBoundary
βœ“ Valid Pattern
  • βœ“Server imports and renders Client Components
  • βœ“Pass Server data to Client via serializable props
  • βœ“Nest Client Components anywhere in the Server tree
  • βœ“Pass Server Components as children to Client wrappers
app/page.tsx
// Server Component β€” no directive needed
import { FavoriteButton } from "@/components/FavoriteButton"

async function Page() {
  const  data = await fetchPokemon();
  return (
    <div>
      <PokemonCard data={data} />  {/* Server */}
      <FavoriteButton />  {/* Client βœ“ */}
    </div>
  );
}
βœ— Invalid Pattern
  • βœ—Client cannot import a Server Component directly
  • βœ—Server-only code would be bundled into the client
  • βœ—Database calls inside Client components break at runtime
  • βœ—Fix: pass Server Component as children prop instead
components/ClientWrapper.tsx
"use client"
import { ServerFeed } from "@/components/ServerFeed"  // βœ—

function ClientWrapper() {
  // βœ— ServerFeed gets pulled into client bundle
  return <ServerFeed />
}

// βœ“ Fix β€” accept as children instead
function ClientWrapper({ children }) {
  return <div>children</div>
}
πŸ’‘

Serialization Boundary

All props crossing from Server β†’ Client must be serializable JSON. No functions, class instances, Symbols, Maps, or Sets. Use plain objects, arrays, strings, numbers, and booleans. Server Actions are the only exception β€” React serializes them as references.

Client Components

React Query for client-side data fetching with caching

Client-Side

Error & Loading Patterns

Build resilient UIs with Suspense boundaries and Error Boundaries

Understanding Boundaries
SuspenseLoading States

What it does:

Shows a fallback UI while async components are loading. Enables streaming and progressive rendering.

When to use:

Wrap async Server Components to show loading skeletons while data fetches.

<Suspense fallback={<Skeleton />}>
  <AsyncComponent />
</Suspense>
Error BoundaryError States

What it does:

Catches JavaScript errors in child components and displays a fallback UI instead of crashing.

When to use:

Wrap components that might fail (network errors, missing data) to prevent whole-page crashes.

<ErrorBoundary fallback={<Error />}>
  <ComponentThatMightFail />
</ErrorBoundary>

πŸ’‘ Combine Both for Resilience

Use Suspense for loading states and Error Boundaries for failures. This pattern ensures users see appropriate feedback whether components are loading or have failed.

Single Boundary
All-or-nothing

One boundary around multiple components. If any fails, the entire section shows an error.

<ErrorBoundary>
  <Suspense fallback={<Loading />}>
    <Card1 />
    <Card2 />
    <Card3 />
  </Suspense>
</ErrorBoundary>

Behavior:

  • β€’All cards wait until slowest one loads
  • β€’If one fails, entire section fails
  • β€’Simpler code, less granular UX
Individual Boundaries
Isolated

Separate boundaries for each component. Failures and loading are isolated to individual cards.

{cards.map(card => (
  <ErrorBoundary key={card.id}>
    <Suspense fallback={<Skeleton />}>
      <Card {...card} />
    </Suspense>
  </ErrorBoundary>
))}

Behavior:

  • β€’Each card streams in as it's ready
  • β€’Failed cards don't affect others
  • β€’Better UX, more complex code
Live Demo: Partial Failures

Each card has ~30% chance to fail. Refresh to see different outcomes.

Individual Boundaries

Notice: Some cards may show errors while others load successfully. This is the power of isolated boundariesβ€”failures don't cascade. Without individual boundaries, one error would break the entire grid.

Technical Reference

Understanding the patterns and best practices

⚑

Parallel Fetching

Multiple independent requests execute simultaneously for optimal performance

Best for:

  • β€’ Independent data sources
  • β€’ Maximum speed
  • β€’ Unrelated requests
πŸ”—

Sequential Fetching

Requests execute one after another when dependencies exist

Best for:

  • β€’ Dependent requests
  • β€’ Ordered execution
  • β€’ Rate limiting
🌊

Streaming

Progressive rendering as data becomes available

Best for:

  • β€’ Better perceived performance
  • β€’ Content cards
  • β€’ User profiles
🧭

Try Navigation & Partial Updates

See how layouts persist while content streams in. Experience the power of partial rendering.

Explore Navigation Demo β†’