Nathaniel's blog
Back to posts

React Server Components: A Deep Dive

Nathaniel LinJanuary 22, 202610 min read1 views
React Server Components: A Deep Dive

React Server Components (RSC) represent the biggest architectural shift in React since hooks. They let you run components on the server, stream the result to the client, and skip shipping their JavaScript entirely. But the mental model takes some getting used to.

The Core Idea

Every React component you've ever written is a Client Component — it runs in the browser, handles state, and responds to events. Server Components flip this: they run only on the server, have direct access to databases and filesystems, and send rendered UI (not code) to the client.

// This component never ships to the browser
async function RecentPosts() {
  const posts = await db.query.posts.findMany({
    limit: 10,
    orderBy: [desc(posts.createdAt)],
  });

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>
          <a href={`/posts/${post.slug}`}>{post.title}</a>
        </li>
      ))}
    </ul>
  );
}

No useEffect. No loading state. No API route. The component awaits the database directly, renders HTML, and streams it to the browser.

The Boundary Rules

The key constraint is the "use client" directive. Once you mark a file with "use client", everything it imports is also a Client Component. The boundary flows downward:

  • Server Components can render Client Components

  • Client Components cannot import Server Components (but can accept them as children props)

  • Server Components can pass serializable props to Client Components

This leads to a natural architecture: Server Components own data fetching and layout, Client Components own interactivity.

Practical Patterns

The Wrapper Pattern

When you need interactivity around server-fetched data:

// Server Component
async function PostPage({ slug }: { slug: string }) {
  const post = await getPost(slug);
  return (
    <article>
      <h1>{post.title}</h1>
      <LikeButton postId={post.id} initialCount={post.likeCount} />
      <div dangerouslySetInnerHTML={{ __html: post.html }} />
    </article>
  );
}

LikeButton is a Client Component with "use client" — it handles click events and optimistic updates. But it receives its initial data from the server, avoiding a waterfall.

Streaming with Suspense

Server Components integrate with <Suspense> for progressive rendering:

export default function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>
      <Suspense fallback={<Skeleton />}>
        <SlowAnalyticsWidget />
      </Suspense>
      <Suspense fallback={<Skeleton />}>
        <RecentActivity />
      </Suspense>
    </div>
  );
}

The shell renders instantly. Each <Suspense> boundary streams in as its data resolves. No client-side loading spinners — the HTML arrives progressively.

When to Use What

NeedUse
Database queriesServer Component
Click handlers / formsClient Component
Static layoutServer Component
Animation / stateClient Component
Auth checks (server-side)Server Component
Real-time updatesClient Component

Performance Wins

In our blog, switching to RSC dropped the client JavaScript bundle by 40%. Pages that previously loaded a GraphQL client, parsed responses, and managed cache now simply render on the server. The browser receives HTML, hydrates the interactive bits, and is done.

The trade-off is complexity in understanding the boundary. But once it clicks, RSC feels like the way React was always meant to work — components that just render, wherever they need to.

Share this post

Reactions