DEV Community

Atlas Whoff
Atlas Whoff

Posted on

React Server Components: What Actually Changes in Your Architecture

Server Components Aren't Just an Optimization

Most articles treat React Server Components (RSC) as a performance feature. They're not—they're an architectural shift in where your application logic lives.

Understanding this changes how you design everything.

The Core Distinction

// Server Component — runs on server, never shipped to browser
// app/users/page.tsx
async function UsersPage() {
  // Direct DB access — no API needed
  const users = await db.users.findMany({
    where: { active: true },
    orderBy: { createdAt: 'desc' },
  });

  return (
    <div>
      {users.map(user => (
        <UserCard key={user.id} user={user} />
      ))}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode
// Client Component — runs in browser, can use hooks and events
'use client';

function UserCard({ user }: { user: User }) {
  const [expanded, setExpanded] = useState(false);

  return (
    <div onClick={() => setExpanded(!expanded)}>
      <h3>{user.name}</h3>
      {expanded && <p>{user.email}</p>}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

What You Can't Do in Server Components

// ❌ All of these fail in Server Components:
'use server'; // not needed, this isn't the directive

import { useState, useEffect, useCallback } from 'react'; // ❌
import { useRouter } from 'next/navigation'; // ❌

function MyComponent() {
  const [count, setCount] = useState(0);  // ❌ no hooks

  return <button onClick={() => setCount(c => c + 1)}>{count}</button>; //  no event handlers
}
Enter fullscreen mode Exit fullscreen mode

Server Components are for data fetching and rendering. Client Components are for interactivity.

The Composition Pattern

// Server Component fetches data
async function ProductPage({ params }: { params: { id: string } }) {
  const product = await db.products.findUnique({
    where: { id: params.id },
    include: { reviews: true, images: true },
  });

  if (!product) notFound();

  return (
    <div>
      <ProductImages images={product.images} />   {/* Server Component */}
      <ProductDetails product={product} />          {/* Server Component */}
      <AddToCartButton productId={product.id} />   {/* Client Component */}
      <ReviewList reviews={product.reviews} />     {/* Server Component */}
      <WriteReview productId={product.id} />       {/* Client Component */}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Most of the page is Server Components. Only the interactive parts are Client Components. The result: less JavaScript shipped, faster initial load.

Data Fetching Patterns

Parallel Fetching (avoid waterfalls)

// ❌ Sequential — slow
async function Dashboard() {
  const user = await getUser();        // 100ms
  const orders = await getOrders();    // 100ms
  const analytics = await getStats();  // 100ms
  // Total: 300ms
}

// ✅ Parallel — fast
async function Dashboard() {
  const [user, orders, analytics] = await Promise.all([
    getUser(),
    getOrders(),
    getStats(),
  ]);
  // Total: 100ms (limited by slowest)
}
Enter fullscreen mode Exit fullscreen mode

Streaming with Suspense

import { Suspense } from 'react';

async function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>

      {/* Renders immediately */}
      <QuickStats />  

      {/* Streams in as data loads */}
      <Suspense fallback={<OrdersSkeleton />}>
        <RecentOrders />
      </Suspense>

      <Suspense fallback={<ChartSkeleton />}>
        <RevenueChart />
      </Suspense>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Users see content progressively instead of waiting for the slowest query.

Caching in RSC

import { cache } from 'react';

// Deduplicate identical requests within a render
const getUser = cache(async (id: string) => {
  return db.users.findUnique({ where: { id } });
});

// Called in multiple components — only one DB query
await getUser('123'); // hits DB
await getUser('123'); // returns cached result
Enter fullscreen mode Exit fullscreen mode
// Next.js fetch caching
async function getPosts() {
  const res = await fetch('https://api.example.com/posts', {
    next: { revalidate: 3600 }, // revalidate every hour
  });
  return res.json();
}
Enter fullscreen mode Exit fullscreen mode

When to Use Each

Server Components:

  • Fetching data from DB or APIs
  • Accessing server-only resources (env vars, secrets)
  • Large dependencies you don't want in the client bundle
  • Static or semi-static content

Client Components:

  • Interactive UI (buttons, forms, toggles)
  • Browser APIs (localStorage, geolocation)
  • Third-party libraries that use browser APIs
  • Real-time updates (WebSocket, SSE)

The Mental Model Shift

Old model: fetch data in API routes, pass to components via props or state.

New model: components fetch their own data directly. Server Components are closer to server-rendered templates; Client Components are islands of interactivity.

This eliminates a ton of API route boilerplate for data you only need on the server.


Next.js 14 App Router with RSC patterns, streaming, and optimized data fetching: Whoff Agents AI SaaS Starter Kit.

Top comments (0)