Ideas Engineered for Tomorrow
We Engineer Services & Solutions for Your Business Needs
Home About
Products
Services
Hire
Industries
Consulting
Partners
Articles Careers Contact
Software Development

React Server Components: The Future of React

RSC changes how React works fundamentally. Here's what you actually need to know — the mental model, the boundaries, and when to use server vs client components.

November 11, 2025 12 min read
In this article

React Server Components confused most of the React community when they launched. The mental model shift is real — after a decade of "everything runs in the browser," RSC asks you to think about where each component renders. But once it clicks, it's a genuinely better architecture for most web applications.

We've migrated two client projects to Next.js App Router (which uses RSC) at Pillai Infotech. One went smoothly. One was painful. This guide covers the lessons from both.

What React Server Components Actually Do

React Server Components render on the server and send the result to the client as a serialized React tree — not HTML, but a compact representation that React can merge into the existing component tree without losing client-side state.

The key difference from traditional SSR:

  • Traditional SSR: Render HTML on server → send to client → hydrate (re-attach JavaScript to every component)
  • RSC: Server Components render on server and stay on the server. Only Client Components are hydrated. Server Component code never ships to the browser.
The real benefit: A Server Component that imports a 500KB charting library? That library never reaches the browser. The component renders on the server, the output (HTML + data) goes to the client, but the JavaScript stays server-side. This is how RSC reduces bundle size.

The Mental Model: Think in Boundaries

In the RSC world, your component tree has a boundary between server and client. Everything above the boundary runs on the server. Everything below runs on the client.

// app/page.tsx — Server Component (default in Next.js App Router)
// Can: access database, read files, use secrets
// Cannot: useState, useEffect, onClick, browser APIs

import { db } from '@/lib/db';
import { ProductList } from './product-list'; // Server Component
import { AddToCart } from './add-to-cart';     // Client Component

export default async function ProductPage() {
  // This runs on the server — direct DB access, no API needed
  const products = await db.product.findMany({
    where: { active: true }
  });

  return (
    <main>
      <h1>Products</h1>
      <ProductList products={products} />
      {/* Client boundary starts here */}
      <AddToCart products={products} />
    </main>
  );
}
// app/add-to-cart.tsx — Client Component
'use client'; // This directive marks the boundary

import { useState } from 'react';

export function AddToCart({ products }) {
  const [cart, setCart] = useState([]);

  return (
    <button onClick={() => setCart(prev => [...prev, products[0]])}>
      Add to Cart ({cart.length})
    </button>
  );
}

Server vs Client Components: When to Use Each

Need Server Component Client Component
Fetch data from DB/API✔ Direct accessVia fetch/SWR
Use secrets/env vars✔ Safe✘ Never
useState / useEffect
Event handlers (onClick)
Browser APIs (window, localStorage)
Render static content✔ Zero JS shippedJS hydration overhead
Heavy dependencies (markdown, charts)✔ Keeps bundle smallShips entire library

Rule of thumb: Start with Server Components. Only add 'use client' when you need interactivity (state, effects, event handlers, browser APIs). Push the client boundary as far down the component tree as possible.

Data Fetching Patterns

// Pattern 1: Async Server Component (recommended)
async function UserProfile({ userId }: { userId: string }) {
  const user = await db.user.findUnique({ where: { id: userId } });
  return <div>{user.name}</div>;
}

// Pattern 2: Parallel data fetching
async function Dashboard() {
  // These run in parallel — no waterfall
  const [stats, orders, users] = await Promise.all([
    getStats(),
    getRecentOrders(),
    getActiveUsers(),
  ]);

  return (
    <>
      <StatsPanel stats={stats} />
      <OrdersTable orders={orders} />
      <UsersList users={users} />
    </>
  );
}

// Pattern 3: Streaming with Suspense
export default function Page() {
  return (
    <main>
      <h1>Dashboard</h1>
      {/* Shows immediately */}
      <FastStats />

      {/* Streams in when ready — shows loading skeleton first */}
      <Suspense fallback={<OrdersSkeleton />}>
        <SlowOrders />
      </Suspense>
    </main>
  );
}

Server Actions: Mutations Without API Routes

// Server Action — runs on the server, called from client
'use server';

import { revalidatePath } from 'next/cache';
import { z } from 'zod';

const schema = z.object({
  title: z.string().min(1).max(200),
  content: z.string().min(10),
});

export async function createPost(formData: FormData) {
  const data = schema.parse({
    title: formData.get('title'),
    content: formData.get('content'),
  });

  await db.post.create({ data });
  revalidatePath('/posts');  // Refresh the page data
}

// Usage in a Client Component
'use client';
import { createPost } from './actions';

function NewPostForm() {
  return (
    <form action={createPost}>
      <input name="title" required />
      <textarea name="content" required />
      <button type="submit">Create Post</button>
    </form>
  );
  // Works without JavaScript! Progressive enhancement built in.
}

Performance Impact

Metric Before RSC (Pages Router) After RSC (App Router)
JS Bundle (typical page)~120-200 KB~60-100 KB
TTFB~200-400ms~100-200ms (streaming)
LCP~1.8-2.5s~1.0-1.5s
Data fetching waterfallsCommon (client-side)Eliminated (server-side)

For more on optimizing these metrics, see our web performance optimization guide.

Common Pitfalls

  1. Making everything a Client Component. Adding 'use client' to the root layout defeats the purpose. Only interactive components need to be Client Components.
  2. Passing functions as props across the boundary. You can't pass functions from Server to Client Components — only serializable data (strings, numbers, objects, arrays).
  3. Over-fetching in layouts. Layout components render once and don't re-fetch on navigation. Don't put page-specific data fetching in layouts.
  4. Ignoring caching. Next.js caches aggressively. Understanding revalidatePath, revalidateTag, and unstable_cache is essential for correct data freshness.
  5. Not using Suspense boundaries. Without Suspense, slow data fetching blocks the entire page. Wrap slow components in Suspense for streaming.
Building React applications? See our Svelte vs React comparison, Next.js vs Nuxt.js guide, and TypeScript best practices. Or explore our web development services.

Frequently Asked Questions

Do I need Next.js to use React Server Components?
Currently, yes — Next.js is the only production-ready RSC implementation. React's documentation recommends using a framework. Remix and Waku are working on RSC support. Using RSC without a framework is possible but not recommended for production.
Should I migrate from Pages Router to App Router?
Only if you have a clear benefit — reduced bundle size, better data fetching, or streaming needs. The migration is non-trivial and changes your data fetching patterns entirely. Both routers are supported. Migrate incrementally (page by page) rather than all at once.
Are Server Actions secure?
Server Actions are server-side functions exposed as HTTP endpoints. Treat them like any API endpoint: validate all inputs, check authentication, authorize actions. The 'use server' directive doesn't add security — it only marks the function as callable from the client.
How do RSC affect testing?
Server Components are easier to test in some ways — they're just async functions returning JSX. But testing the server/client boundary, streaming behavior, and cache invalidation adds new complexity. We test Server Components as functions (unit tests) and use Playwright for full integration testing.
Is RSC just PHP with extra steps?
This is a common joke, and there's a kernel of truth. Server-rendered HTML with sprinkles of client interactivity is indeed the PHP model. But RSC adds component composition, streaming, seamless client-side navigation, and fine-grained hydration boundaries — capabilities that traditional server rendering doesn't have.
Pillai Infotech Frontend Team
React, Next.js & Modern Frontend Architecture

We build React applications with Server Components, streaming SSR, and modern data patterns. Explore our web development services.