Ishan Dubey
Bangalore, IN
HomeWritingUnderstanding React Server Components
Modern React

Understanding React Server Components

A comprehensive guide to React Server Components and how they change the way we build React applications.

Published
March 20, 2026
Reading time
5 min read
Author
Ishan Dubey

React Server Components (RSC) represent one of the most significant shifts in React's architecture since hooks. After migrating several applications to Next.js App Router, I want to share what I've learned about effectively using Server Components.

The Problem They Solve

Traditional React applications bundle everything—components, libraries, data fetching logic—into a JavaScript bundle sent to the client. This leads to:

  • Large bundle sizes
  • Slow initial page loads
  • Waterfall data fetching
  • Exposed server secrets in client code

Server Components solve these problems by running exclusively on the server.

Server vs Client Components

Server Components

  • Run on the server, never in the browser
  • Can access server-side resources (databases, file systems)
  • Zero JavaScript sent to client
  • Can be async functions
tsx
// Server Component - runs on server only
async function BlogPosts() {
  // Direct database access!
  const posts = await db.posts.findMany();

  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

Client Components

  • Run in the browser
  • Can use hooks and browser APIs
  • Interact with the DOM
  • Handle user interactions
tsx
'use client';

import { useState } from 'react';

function LikeButton({ postId }: { postId: string }) {
  const [likes, setLikes] = useState(0);

  return (
    <button onClick={() => setLikes(l => l + 1)}>
      ❤️ {likes}
    </button>
  );
}

The Mental Model

Think of your application as a tree. Server Components form the trunk and branches, while Client Components are the leaves that need interactivity.

Data Fetching Patterns

Pattern 1: Direct Database Queries

The most powerful aspect of Server Components is direct data access:

tsx
async function UserProfile({ userId }: { userId: string }) {
  // No API route needed!
  const user = await db.user.findUnique({
    where: { id: userId },
    include: { posts: true, comments: true }
  });

  if (!user) {
    notFound();
  }

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.bio}</p>
      <PostCount count={user.posts.length} />
    </div>
  );
}

Pattern 2: Parallel Data Fetching

Fetch independent data in parallel for better performance:

tsx
async function Dashboard() {
  // These fetch simultaneously
  const userPromise = getUser();
  const postsPromise = getPosts();
  const statsPromise = getStats();

  const [user, posts, stats] = await Promise.all([
    userPromise,
    postsPromise,
    statsPromise
  ]);

  return (
    <div>
      <UserCard user={user} />
      <PostList posts={posts} />
      <StatsWidget stats={stats} />
    </div>
  );
}

Pattern 3: Streaming with Suspense

Use Suspense boundaries to stream content progressively:

tsx
import { Suspense } from 'react';

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

      {/* This loads immediately */}
      <StaticContent />

      {/* This streams in when ready */}
      <Suspense fallback={<LoadingSpinner />}>
        <SlowWidget />
      </Suspense>

      {/* This streams independently */}
      <Suspense fallback={<Skeleton />}>
        <AnotherSlowWidget />
      </Suspense>
    </div>
  );
}

Common Pitfalls

1. Over-using Client Components

Don't mark everything as 'use client'. Start with Server Components and only add the directive when you need:

  • Event handlers (onClick, onChange)
  • Browser APIs (localStorage, window)
  • React hooks (useState, useEffect)

2. Passing Functions as Props

You can't pass functions from Server to Client Components:

tsx
// ❌ This won't work
<ClientComponent onClick={() => console.log('clicked')} />

// ✅ Instead, handle it in the client component
<ClientComponent />

3. Importing Server Code in Client Components

Be careful about what you import:

tsx
'use client';

// ❌ Don't do this - exposes DB credentials!
import { db } from './db';

// ✅ Create a Server Action instead
import { updateUser } from './actions';

Server Actions

Server Actions let you call server functions from Client Components:

tsx
'use server';

export async function createPost(formData: FormData) {
  const title = formData.get('title');
  const content = formData.get('content');

  // Server-side validation
  if (!title || typeof title !== 'string') {
    throw new Error('Title is required');
  }

  // Direct database mutation
  await db.post.create({
    data: { title, content }
  });

  revalidatePath('/posts');
}

Used in a Client Component:

tsx
'use client';

export function CreatePostForm() {
  return (
    <form action={createPost}>
      <input name="title" placeholder="Title" />
      <textarea name="content" placeholder="Content" />
      <button type="submit">Create Post</button>
    </form>
  );
}

Performance Benefits

After migrating to Server Components, we saw:

  • 60% reduction in JavaScript bundle size
  • 40% faster Time to Interactive
  • Zero client-side data fetching waterfalls
  • Better SEO with server-rendered content

When to Use What

| Use Server Components | Use Client Components | |----------------------|----------------------| | Static content | Interactive elements | | Data fetching | Event handlers | | Backend integration | Browser APIs | | SEO-critical pages | Animations | | Large dependencies | Real-time updates |

Final Thoughts

React Server Components aren't just a new feature—they're a new way of thinking about React architecture. Embrace the server-first approach, and your applications will be faster, more secure, and easier to maintain.

Start with Server Components everywhere, then progressively enhance with Client Components only where interactivity is needed.

Table of Contents