Back to blog

React Server Components: The Ultimate Guide to Understanding the Biggest Change in React in 2025

Hello HaWkers, React Server Components (RSC) represent the most significant evolution of React since hooks. In 2025, frameworks like Next.js 14+ made RSC mainstream, completely changing how we think about rendering and application architecture.

Have you ever wondered why your React applications load so much JavaScript in the client? And what if you could execute components directly on the server, without sending code to the browser?

What Are React Server Components?

React Server Components are components that run exclusively on the server. Unlike traditional Server-Side Rendering (SSR), RSC are not hydrated in the client - they simply render HTML and send it to the browser, along with instructions about where interactive components (Client Components) should be inserted.

Diferença fundamental:

  • SSR: Renders on the server, sends HTML, but also sends JavaScript to "hydrate" the component in the client
  • RSC: Renders on the server, sends only the result (serialized HTML), zero JavaScript for that component in the client

This means dramatically smaller JavaScript bundles and much better performance, especially on slower devices.

Architecture: Server vs Client Components

The new React architecture divides components into two categories:

Server Components (default)

// app/ProductList.jsx (Server Component by default)
import { db } from '@/lib/database';

export default async function ProductList() {
  // Direct database access - no intermediate API!
  const products = await db.query('SELECT * FROM products WHERE active = true');

  return (
    <div className="product-grid">
      {products.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
}

Features:

  • Can be async
  • Direct access to database, filesystem, private APIs
  • Zero JavaScript sent to the client
  • Cannot use hooks (useState, useEffect, etc)
  • Cannot have event handlers

Client Components

'use client'; // Required directive

import { useState } from 'react';

export default function AddToCartButton({ productId }) {
  const [loading, setLoading] = useState(false);

  const handleClick = async () => {
    setLoading(true);
    await fetch(`/api/cart/add`, {
      method: 'POST',
      body: JSON.stringify({ productId })
    });
    setLoading(false);
  };

  return (
    <button onClick={handleClick} disabled={loading}>
      {loading ? 'Adding...' : 'Add to Cart'}
    </button>
  );
}

Features:

  • Need the directive 'use client'
  • Can use hooks and state
  • Can have interactivity
  • JavaScript sent to the client
  • Work like traditional React components

Composition: Mixing Server and Client Components

The real power comes from composition. Server Components can render Client Components, and Client Components can receive Server Components as children:

// app/ProductPage.jsx (Server Component)
import ProductDetails from './ProductDetails'; // Server
import AddToCartButton from './AddToCartButton'; // Client
import Reviews from './Reviews'; // Server

export default async function ProductPage({ params }) {
  const product = await db.products.findUnique({
    where: { id: params.id },
    include: { reviews: true }
  });

  return (
    <div className="product-page">
      {/* Server Component - no JS in the client */}
      <ProductDetails product={product} />

      {/* Client Component - with interactivity */}
      <AddToCartButton productId={product.id} />

      {/* Server Component - static list */}
      <Reviews reviews={product.reviews} />
    </div>
  );
}

React Server Components architecture

Data Fetching: A Nova Era

With RSC, data fetching is dramatically simpler. No longer need getServerSideProps, getStaticProps, or complex state managers:

// app/dashboard/page.jsx
async function getData() {
  // Fetch directly, can be cached
  const [user, stats, notifications] = await Promise.all([
    db.user.findUnique({ where: { id: userId } }),
    db.analytics.aggregate({ userId }),
    db.notifications.findMany({ userId, unread: true })
  ]);

  return { user, stats, notifications };
}

export default async function Dashboard() {
  const { user, stats, notifications } = await getData();

  return (
    <div className="dashboard">
      <UserProfile user={user} />
      <StatsWidget stats={stats} />
      <NotificationBell count={notifications.length}>
        {/* Server Component passed as children */}
        <NotificationList notifications={notifications} />
      </NotificationBell>
    </div>
  );
}

Vantagens:

  • Colocation: fetch near where it is used
  • Parallel fetching automático
  • No waterfalls
  • Native React cache
  • No complex loading states

Streaming e Suspense

RSC integrates perfectly with Suspense for progressive streaming:

import { Suspense } from 'react';

export default function Page() {
  return (
    <div>
      {/* Static content renders immediately */}
      <Header />

      {/* Suspense allows streaming of slow parts */}
      <Suspense fallback={<ProductListSkeleton />}>
        <ProductList />
      </Suspense>

      <Suspense fallback={<ReviewsSkeleton />}>
        <Reviews />
      </Suspense>

      <Footer />
    </div>
  );
}

async function ProductList() {
  // Slow query (3 seconds)
  const products = await slowDatabaseQuery();
  return <div>{/* render products */}</div>;
}

async function Reviews() {
  // External slow API (2 seconds)
  const reviews = await fetch('https://api.reviews.com/...');
  return <div>{/* render reviews */}</div>;
}

The browser receives:

  1. Immediately: Header + skeletons
  2. After 2s: Reviews (via streaming)
  3. After 3s: ProductList (via streaming)

The user sees content progressively, without waiting for everything to load.

Performance Optimization

RSC bring significant performance gains:

Reduction of Bundle

Real example of migration from Next.js 13 → Next.js 14 with RSC:

Before (Client Components):

  • Bundle JS: 850KB
  • First Contentful Paint: 2.1s
  • Time to Interactive: 3.8s

After (Server Components):

  • Bundle JS: 120KB (-85%)
  • First Contentful Paint: 0.8s (-62%)
  • Time to Interactive: 1.2s (-68%)

Elimination of Heavy Dependencies

// Server Component - zero impact on the bundle
import { marked } from 'marked'; // 50KB
import hljs from 'highlight.js'; // 150KB
import { formatDate } from 'date-fns'; // 25KB

export default async function BlogPost({ slug }) {
  const post = await getPost(slug);
  const html = marked(post.content);
  const highlighted = hljs.highlightAuto(html);

  return (
    <article>
      <time>{formatDate(post.date, 'PPP')}</time>
      <div dangerouslySetInnerHTML={{ __html: highlighted.value }} />
    </article>
  );
}

// These 225KB of dependencies will NOT go to the client!

Patterns and Best Practices

1. Move Interactivity to the Leaves

// ❌ Bad: Client Component at the top
'use client';

export default function ProductPage({ product }) {
  return (
    <div>
      <ProductImage src={product.image} /> {/* No need to be client */}
      <ProductDetails data={product} /> {/* No need to be client */}
      <AddToCartButton productId={product.id} /> {/* Need to be client */}
    </div>
  );
}

// ✅ Good: Client Component only where needed
export default function ProductPage({ product }) {
  return (
    <div>
      <ProductImage src={product.image} /> {/* Server */}
      <ProductDetails data={product} /> {/* Server */}
      <AddToCartButton productId={product.id} /> {/* Client */}
    </div>
  );
}

2. Pass Server Components as Props

// Client Component
'use client';

export default function Tabs({ children }) {
  const [tab, setTab] = useState(0);

  return (
    <div>
      <TabButtons active={tab} onChange={setTab} />
      <div>{children[tab]}</div>
    </div>
  );
}

// Server Component
export default function Page() {
  return (
    <Tabs>
      {/* Each tab is Server Component */}
      <ProductList />
      <ReviewsList />
      <SpecsList />
    </Tabs>
  );
}

3. Strategic Composition

Server Components should do the heavy work (data fetching, computation), while Client Components only deal with interactivity.

Challenges and Limitations

RSC are not a silver bullet. There are challenges:

1. Learning Curve: Different paradigm requires rethinking architecture.

2. Complex Debugging: Errors can happen on the server or client, complicating troubleshooting.

3. Context Limitations: Context API does not work between server and client boundaries.

4. Serialization: Props passed from Server → Client must be serializable (no functions, classes, etc).

5. Complex Caching: React cache system is powerful but can be confusing.

The Future of React

React Server Components are the future. In 2025:

  • Next.js App Router made RSC the default
  • Remix is adopting similar architecture
  • Other React frameworks will follow

Expected benefits:

  • Applications faster by default
  • Bundles menores automaticamente
  • Better native SEO
  • Simpler architecture

If you are inspired by React Server Components, I recommend another article: Server-First Development: Como SvelteKit, Astro e Remix Estão Redefinindo o Desenvolvimento Web where you will discover other server-first approaches.

Let's go! 🦅

Complete Study Material

If you want to master JavaScript from basics to advanced, I've prepared a complete guide:

Investment options:

  • $4.90 (single payment)

👉 Learn About JavaScript Guide

💡 Material updated with industry best practices

Comments (0)

This article has no comments yet 😢. Be the first! 🚀🦅

Add comments