React Server Components: Zero-JS Components

Medium

React Server Components let you render parts of your tree ahead of time in a separate server environment, before bundling them into client JavaScript. The biggest win is not that every page becomes “static,” but that only truly interactive code must cross the server-client boundary.

Quick Decision Guide

React Server Components keep non-interactive UI on the server and out of the client bundle.

- Use Server Components for data fetching, server-only code, and non-interactive UI - Use Client Components for state, effects, event handlers, and browser APIs - In Next.js App Router, components are server by default unless a 'use client' boundary opts them into client behavior

Interview signal: RSC is not just about faster HTML. It is about reducing how much JavaScript the browser must download, parse, and hydrate.

What are React Server Components?

React Server Components (RSC) are components that render in a server environment ahead of time, before bundling, and do not themselves become hydrated client code.

🔥 Insight

RSC is a bundle-boundary feature as much as a rendering feature. The question is not only where code runs, but whether that code needs to ship to the browser at all.

Key Characteristics

render in a separate server environment
can access server-side resources directly
do not use client-only features like browser events or local state hooks
can compose with Client Components when interactivity is needed

Important Clarification

Server Components are not the same thing as classic SSR components.

SSR is about generating HTML on the server
RSC is about keeping component logic on the server and out of the client bundle

These often work together, but they solve different problems.

What Problem They Solve

Without RSC, many components that only fetch and display data still end up in the client-side React tree.

That leads to:

larger bundles
more hydration work
more browser CPU cost

RSC reduces that waste by keeping non-interactive logic on the server.

Server Components vs Client Components

In frameworks that support RSC, the tree is server-first by default, and 'use client' creates a client boundary in the module dependency graph.

Server Components

// app/page.jsx
export default async function Page() {
  const res = await fetch('https://api.example.com/data');
  const data = await res.json();

  return <div>{data.content}</div>;
}

Good fit for Server Components

data fetching
rendering static or read-only UI
accessing databases or ORMs
reading from the filesystem in supported server runtimes
keeping secrets and server-only code off the client

Not allowed / not appropriate in Server Components

useState
useEffect
browser APIs like window, document, localStorage
click handlers like onClick

Client Components

'use client';

import { useState } from 'react';

export default function Button() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

Good fit for Client Components

event handlers
local state
effects
browser APIs
interactive third-party libraries

🔥 Mental Model

Server Component: compute and render on the server, send result
Client Component: send code to the browser so React can run it there

Important Clarification

'use client' does not mark just one component function. It creates a client boundary for that module and the client-side subtree it pulls in.

Data Fetching with Server Components

One of the biggest RSC benefits is that data fetching can happen directly in the server component itself.

Direct Database or ORM Access

import { db } from '@/lib/db';

export default async function ProductsPage() {
  const products = await db.products.findMany();

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

Fetch API

export default async function PostsPage() {
  const res = await fetch('https://api.example.com/posts', {
    cache: 'no-store'
  });
  const posts = await res.json();

  return <PostsList posts={posts} />;
}

Parallel Fetching

export default async function Page() {
  const [users, posts, comments] = await Promise.all([
    fetchUsers(),
    fetchPosts(),
    fetchComments()
  ]);

  return (
    <div>
      <UsersList users={users} />
      <PostsList posts={posts} />
      <CommentsList comments={comments} />
    </div>
  );
}

Why This Matters

Fetching on the server helps avoid pushing unnecessary request orchestration into the client. It also makes server-side parallelism much easier to express.

Important Nuance

RSC does not mean “never call an API.” It means you can fetch in the server environment directly, using the best server-side primitive for your stack: fetch, a database/ORM, or filesystem access in supported runtimes.

Composition Patterns

The common pattern is: keep the outer structure and data-heavy parts on the server, then insert small interactive Client Components where needed.

Server Component Composing a Client Component

// Server Component
import InteractiveButton from '@/components/Button';

export default async function Page() {
  const product = await fetchProduct();

  return (
    <div>
      <h1>{product.name}</h1>
      <ProductDescription product={product} />
      <InteractiveButton />
    </div>
  );
}
// Client Component
'use client';

export default function InteractiveButton() {
  return <button>Add to cart</button>;
}

Passing Server Data to a Client Component

// Server Component
export default async function Page() {
  const data = await fetchData();
  return <ClientComponent data={data} />;
}
'use client';

export default function ClientComponent({ data }) {
  return <div>{data.title}</div>;
}

Common Mistake

Trying to use client-only hooks inside a Server Component:

// ❌ Invalid in a Server Component
export default function Page() {
  const [state, setState] = useState(0);
  return <div>{state}</div>;
}

Better Pattern

Move only the interactive part behind a client boundary:

'use client';

export function ClientPart() {
  const [state, setState] = useState(0);
  return <div>{state}</div>;
}
export default function Page() {
  return <ClientPart />;
}

🔥 Insight

A strong RSC architecture does not make the whole page client-side just because one button needs onClick.

Benefits

Benefits of React Server Components

Performance Benefits

less client JavaScript for non-interactive UI
less hydration work in the browser
better fit for server-side data fetching
easier to keep expensive data logic off the client

Developer Benefits

direct access to server resources in supported environments
secrets stay on the server
clearer separation between rendering and interactivity
easier to compose server-rendered content with small client islands

Senior-Level Insight

The biggest win is usually not “zero JS everywhere.” The biggest win is reducing unnecessary client code for components that never needed to be interactive in the first place.

Trade-offs and Misconceptions

Misconception: “RSC means no JavaScript at all”

Not true. Client Components still ship JavaScript, hydrate, and run in the browser.

Misconception: “RSC replaces SSR”

Not true. Server Components and SSR often work together. SSR affects HTML delivery; RSC affects what code becomes part of the client bundle.

Real Trade-offs

more architectural complexity than a fully client-rendered tree
need to think carefully about server-client boundaries
some third-party libraries require client components
serializable props and server/client boundaries affect how data flows

Interview Takeaway

A strong answer sounds like this:

> React Server Components keep non-interactive rendering and data access on the server, while Client Components handle state, events, and browser APIs. The real benefit is smaller bundles and less hydration, not just server rendering.

Key Takeaways

1React Server Components render in a separate server environment and do not themselves add client JavaScript
2Server Components are different from classic SSR; SSR is about HTML generation, while RSC is about server-client bundle boundaries
3In Next.js App Router, components are server by default and 'use client' creates a client boundary
4Use Server Components for data fetching, server-only resources, and non-interactive UI
5Use Client Components for state, effects, browser APIs, and event handlers
6A strong architecture keeps only the truly interactive parts inside client boundaries
7The main win is reducing client bundle size and hydration work, not eliminating all JavaScript