React JS

Understanding React Server Components Explained Simply

By Utility Zone · 2025-11-01T18:06:46.319962

React Server Components (RSC) fundamentally change how React applications render. Instead of sending all component code to the browser, RSC lets you run specific components entirely on the server, sending only the rendered output to the client. This dramatically reduces the JavaScript bundle users must download while improving performance and security.123


How React Server Components Work

By default, all React components in modern frameworks are server components—they render on the server and produce HTML. No client-side JavaScript is shipped for these components. You only mark a component as a client component with the 'use client' directive when it needs browser APIs, state, or interactivity.24

When a server component fetches data, that data flows directly into the component during rendering, then the server sends serialized HTML to the browser. For client components within that tree, React sends them as lightweight JavaScript bundles with instructions on where to attach in the DOM.35


The Three Rendering Approaches

Comparison of RSC, SSR, and Client Components Across Key Metrics

Comparison of RSC, SSR, and Client Components Across Key Metrics

RSC works differently from SSR and pure client-side rendering in fundamental ways:

RSC (React Server Components): Server components never ship their code to the browser. They render completely on the server, and React sends only the rendered output plus lightweight client component bundles. This is ideal for data display, static content, and avoiding client-side waterfall requests.23

SSR (Server-Side Rendering): The server renders HTML on each request, then the client downloads the same component logic to "hydrate" it with interactivity. Both the server and client execute the same component code, meaning full component logic ships to the browser. This improves first paint but still carries substantial bundle overhead.63

Client Components: All rendering and data fetching happen in the browser. This was the traditional React model. Users see a blank screen until JavaScript downloads and executes, then wait for additional API calls to fetch data.2

The key distinction: RSC server code never reaches the browser, while SSR ships all code to both server and client.2

Pros and Cons Comparison

React Server Components Pros:

  • Dramatically smaller client bundles (expensive dependencies stay on server)13
  • Data fetching happens before rendering (no waterfall delays)3
  • Direct database access without API layers (cleaner architecture)71
  • Sensitive credentials never exposed to clients8
  • Seamless streaming and progressive rendering with Suspense3
  • Reduced Time-to-Interactive for users1

React Server Components Cons:

  • Cannot use React hooks (useState, useContext, useEffect)29
  • No access to browser APIs (localStorage, geolocation, etc.)1
  • Mental model shift from traditional component architecture8
  • Limited tooling and third-party library support (still evolving)8
  • Requires compatible framework (Next.js 13+, etc.)8

SSR Pros:

  • Better initial page load than pure CSR6
  • Improved SEO (HTML available immediately)6
  • Reduces Time-to-First-Paint10
  • Works with all React components as-is6

SSR Cons:

  • Server strain under heavy traffic (generates full HTML per request)10
  • Double rendering work (server + client hydration)26
  • Still ships full component logic to browser2
  • Hydration delay before interactivity10

Client Components Pros:

  • Full browser API access2
  • Familiar mental model for interactive features1
  • Instant updates with state changes2

Client Components Cons:

  • Large JavaScript bundles32
  • Slower first page load3
  • Network waterfalls for data fetching63
  • Cannot directly access secure data sources3

Migration Example: Converting a Client Widget to Server Component

The Before State

A typical client component might fetch product data in useEffect, manage loading/error states, and struggle with network waterfalls:

'use client';
import { useState, useEffect } from 'react';

export default function ProductList() {
  const [products, setProducts] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch('/api/products')
      .then(r => r.json())
      .then(data => {
        setProducts(data);
        setLoading(false);
      });
  }, []);

  if (loading) return <div>Loading...</div>;
  return (
    <div>
      {products.map(p => (
        <ProductCard key={p.id} product={p} />
      ))}
    </div>
  );
}

Problems: Extra network request after initial render, all this component's code shipped to browser, useEffect hook overhead, loading state management complexity.

The After State

As a server component with async/await:

// No 'use client' - renders on server by default
import { Suspense } from 'react';
import { getProducts } from '@/lib/products';
import ProductLoadingSkeleton from './ProductLoadingSkeleton';

export default async function ProductList() {
  // Direct await in render - data fetches during rendering
  const products = await getProducts();

  return (
    <div>
      {products.map(p => (
        <ProductCard key={p.id} product={p} />
      ))}
    </div>
  );
}

// Wrap with Suspense for loading UI
export function ProductListWithSuspense() {
  return (
    <Suspense fallback={<ProductLoadingSkeleton />}>
      <ProductList />
    </Suspense>
  );
}

Improvements: Zero client JavaScript for this component, data ready during render (no extra request), cleaner async/await syntax, Suspense handles loading states elegantly.

Data Fetching Layer

Create a server-only module that databases can safely query:

// lib/products.ts - never shipped to client
import { db } from '@/lib/db';

export async function getProducts() {
  return db.product.findMany({
    select: {
      id: true,
      name: true,
      price: true,
    },
  });
}

Splitting Interactivity into Client Components

When interactivity is needed (like "Add to Cart"), extract it into a separate client component that receives serializable props:5

// ProductCard.tsx (Server Component - display only)
import AddToCartButton from './AddToCartButton';

export default function ProductCard({ product }) {
  return (
    <div>
      <h3>{product.name}</h3>
      <span>${product.price}</span>
      {/* Only this part runs on client */}
      <AddToCartButton productId={product.id} />
    </div>
  );
}

// AddToCartButton.tsx (Client Component - interactivity only)
'use client';
import { useState } from 'react';

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

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

  return (
    <button onClick={handleClick}>
      {added ? 'Added!' : 'Add to Cart'}
    </button>
  );
}

This pattern ensures only the button logic ships to the client, while the entire product display renders on the server.5


Project Folder Structure

A well-organized Next.js project using RSC follows this structure:

app/                          # Routes and pages
├── layout.tsx               # Root layout
├── page.tsx                 # Home page
└── products/
    └── page.tsx             # /products route

components/
├── ui/                      # Reusable UI blocks
│   ├── Button/
│   └── Card/
├── features/                # Feature-specific components
│   ├── products/
│   │   ├── ProductList.tsx         # Server
│   │   ├── ProductCard.tsx         # Server
│   │   └── AddToCartButton.tsx     # Client
│   └── dashboard/
└── layout/                  # Page layout sections

lib/                         # Server-safe utilities
├── products.ts            # Data fetching
├── db.ts                  # Database client
└── types.ts               # TypeScript definitions

In this structure, components without 'use client' are server components by default. Only components that need browser interactivity get the directive. The lib/ folder contains server-only code that fetches from databases or secure APIs.1112


Best Practices for RSC Migration

Start with an incremental approach: add 'use client' to your root component, then gradually move the directive lower in the tree as you replace data-fetching logic with async server components. Use Suspense boundaries to handle loading states elegantly—wrap server components with <Suspense fallback={<Skeleton />}> for progressive rendering.131415

Keep serializable props only when passing data from server to client components; avoid functions or class instances. This ensures React can safely transmit data across the server-client boundary.29

Use error boundaries for graceful error handling in server components, and combine them with Suspense for complete async state management. For real-time data that needs updates, pass promises from server to client and resolve them using the use() hook.1617

Remember that framework implementations like Next.js handle the streaming and serialization automatically—you simply write async components and let the framework handle delivery to clients.3 <span style="display:none">181920212223242526272829303132333435363738</span>


<div align="center">⁂</div>

Footnotes

  1. https://www.builder.io/blog/why-react-server-components ↩ ↩2 ↩3 ↩4 ↩5 ↩6

  2. https://www.infozzle.com/blog/react-server-components-rsc-guide-architecture-seo-and-performance/ ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12

  3. https://vercel.com/blog/understanding-react-server-components ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12

  4. https://react.dev/reference/rsc/use-client ↩

  5. https://nextjs.org/docs/app/getting-started/server-and-client-components ↩ ↩2 ↩3

  6. https://dev.to/hasunnilupul/react-server-components-rsc-a-deep-dive-with-examples-and-diagrams-3g4c ↩ ↩2 ↩3 ↩4 ↩5 ↩6

  7. https://nextjs.org/learn/dashboard-app/fetching-data ↩

  8. https://www.codefixeshub.com/react/react-server-components-migration-guide-for-advanc ↩ ↩2 ↩3 ↩4

  9. https://nextjs.org/docs/app/api-reference/directives/use-client ↩ ↩2

  10. https://www.angularminds.com/blog/the-difference-between-rsc-and-ssr-in-react ↩ ↩2 ↩3

  11. https://sentry.io/answers/next-js-directory-organisation-best-practices/ ↩

  12. https://www.wisp.blog/blog/the-ultimate-guide-to-organizing-your-nextjs-15-project-structure ↩

  13. https://www.mux.com/blog/what-are-react-server-components ↩

  14. https://react.dev/reference/react/Suspense ↩

  15. https://codeparrot.ai/blogs/react-suspense-simplifying-asynchronous-rendering-in-react ↩

  16. https://reetesh.in/blog/suspense-and-error-boundary-in-react-explained ↩

  17. https://react.dev/reference/rsc/server-components ↩

  18. https://react.dev/reference/react/Component ↩

  19. https://www.webscope.io/blog/server-components-vs-ssr ↩

  20. https://www.contentful.com/blog/react-server-components-concepts-and-patterns/ ↩

  21. https://www.youtube.com/watch?v=WKfPctdIDek ↩

  22. https://github.com/kriasoft/Folder-Structure-Conventions ↩

  23. https://www.reddit.com/r/nextjs/comments/14llnpj/converting_a_client_component_to_server_client/ ↩

  24. https://nextjs.org/docs/app/getting-started/fetching-data ↩

  25. https://dev.to/wolfflucas/best-practices-for-creating-a-folder-and-file-structure-for-a-react-application-3k1 ↩

  26. https://www.youtube.com/watch?v=glzmT1d8hxM ↩

  27. https://www.robinwieruch.de/next-server-actions-fetch-data/ ↩

  28. https://www.reddit.com/r/projectmanagement/comments/jrmp00/sample_project_folder_structure/ ↩

  29. https://leapcell.io/blog/optimizing-data-fetching-and-caching-with-react-server-components ↩

  30. https://www.dhiwise.com/post/ultimate-guide-to-organizing-your-nextjs-components-folder ↩

  31. https://www.divotion.com/blog/nextjs-react-server-components-make-your-life-easier ↩

  32. https://dev.to/codenextgen/understanding-the-use-client-directive-in-nextjs-13-2oba ↩

  33. https://www.reddit.com/r/nextjs/comments/1dc17tv/best_practice_for_folder_structure_in_nextjs_app/ ↩

  34. https://nextjs-faq.com/sharing-client-side-state-with-server-components ↩

  35. https://www.prisma.io/docs/orm/prisma-client/using-raw-sql/raw-queries ↩

  36. https://stackoverflow.com/questions/76860370/passing-state-values-from-client-component-to-server-component-best-practices-u ↩

  37. https://www.prisma.io/docs/orm/prisma-client/queries ↩

  38. https://www.prisma.io/docs/getting-started/setup-prisma/add-to-existing-project/relational-databases/querying-the-database-node-mysql ↩