CCelestiallines.com
← Back to blog

React Server Components Explained for Beginners

By Pawan Singh · Jun 2026

The Problem RSC Actually Solves

Traditional React (even with server-side rendering) ships every component's JavaScript to the browser — the server might render the initial HTML, but the component code, and everything it imports, still gets bundled and sent for hydration. A component that just fetches data and displays it, with no interactivity of its own, still costs bundle size and client-side execution it doesn't actually need. React Server Components (RSC) let a component run only on the server, never shipping its code to the browser at all — not "rendered on the server, then hydrated," but genuinely absent from the client bundle.

Server Components vs Client Components

In a framework built around RSC (Next.js's App Router is the main one in production use), every component is a Server Component by default, unless explicitly opted out:

// Server Component (default, no directive needed)
async function ProductList() {
    const products = await db.query('SELECT * FROM products');
    return (
        <ul>
            {products.map((p) => <li key={p.id}>{p.name}</li>)}
        </ul>
    );
}

// Client Component — explicit opt-in
'use client';

function LikeButton() {
    const [liked, setLiked] = useState(false);
    return <button onClick={() => setLiked(!liked)}>{liked ? 'Liked' : 'Like'}</button>;
}

Notice the Server Component queries a database directly, with a plain await — no useEffect, no loading state, no client-side data-fetching library. It runs once, on the server, and its rendered output is what gets sent down; there's no re-running it in the browser at all.

Why Server Components Can Do Things Client Components Can't

Because the code never reaches the browser, a Server Component can safely use server-only resources directly — database clients, filesystem access, private API keys and secrets — without any risk of exposing them client-side. A Client Component doing the equivalent would leak that access (or the credentials themselves) into the JavaScript bundle every visitor downloads, which is exactly the class of bug this architecture is designed to make structurally impossible rather than something you have to remember to avoid.

When You Actually Need 'use client'

Add the directive only when a component genuinely needs:

  • State (useState, useReducer) or effects (useEffect)
  • Browser-only APIs (window, localStorage, geolocation)
  • Event handlers (onClick, onChange, and similar)
  • Third-party libraries that themselves rely on any of the above (many UI/animation libraries do)

Everything else should stay a Server Component by default — the performance win compounds the more of a page's total component tree can stay server-only.

Composing Server and Client Components Together

A common, correct pattern: a Server Component fetches data and passes it as props into a Client Component that needs interactivity, rather than making the whole subtree client-side just because one small part of it needs a click handler:

// Server Component
async function ProductPage({ id }) {
    const product = await getProduct(id);
    return (
        <div>
            <h1>{product.name}</h1>
            <AddToCartButton productId={product.id} />
        </div>
    );
}

// Client Component, receives data as a prop rather than fetching it itself
'use client';
function AddToCartButton({ productId }) {
    const [pending, setPending] = useState(false);
    return <button onClick={() => addToCart(productId)}>Add to Cart</button>;
}

This keeps the data-fetching, database-adjacent code server-only while isolating the truly interactive part (the button and its click state) to the smallest possible client bundle.

What You Cannot Do in a Server Component

No hooks that depend on the client (useState, useEffect, useContext for browser-only context), no event handlers, and no browser APIs — attempting any of these in a Server Component is a build-time error, not a silent bug, which is the framework catching the mistake early rather than shipping something broken.

A Common Point of Confusion

A Client Component can still render on the server for the initial page load (hydration) — "Client Component" describes where its code is allowed to run (both server and client), not that it skips server rendering entirely. The real distinction RSC introduces is Server Components, whose code is server-only, period; there's no equivalent "runs everywhere" category for them.

Adopting React Server Components in a new or existing project? Get in touch — I work as a freelance React developer.

Comments