Writing
Web Development3 min read

React Server Components in Production: Patterns That Hold Up

Field-tested React Server Components patterns for Next.js—where to draw the server/client boundary, data fetching, streaming, and the mistakes to avoid.

AGAnsh GuptaFrontend Engineer
WEB

React Server Components (RSC) stopped being a demo and became the default the moment the Next.js App Router matured. After shipping RSC-heavy apps, the wins are real—smaller client bundles, data fetching without waterfalls—but only if you respect the boundary. Here are the patterns that survive contact with a real codebase.

Server by default, client on purpose

In the App Router, every component is a Server Component unless you opt out with "use client". Treat that opt-out as a budget you spend deliberately. A component needs to be a Client Component only when it uses state, effects, refs, browser APIs, or event handlers.

// app/dashboard/page.tsx — Server Component (no directive)
export default async function Page() {
  const invoices = await db.invoice.findMany();
  return <InvoiceTable rows={invoices} />;
}
// components/filter.tsx — Client Component (needs state)
"use client";
import { useState } from "react";
export function Filter() {
  const [q, setQ] = useState("");
  return <input value={q} onChange={(e) => setQ(e.target.value)} />;
}

Push the boundary down, not up

The most common mistake is marking a whole page "use client" because one button deep inside needs interactivity. That drags the entire subtree—and its dependencies—into the client bundle. Instead, keep the page on the server and make the leaf a Client Component. Server Components can render Client Components, so isolate interactivity at the smallest possible node.

Pass data, not functions, across the boundary

Props that cross from Server to Client must be serializable. You can pass strings, numbers, arrays, and plain objects—but not functions, class instances, or Dates without care. When you need server logic from the client, reach for a Server Action rather than trying to smuggle a callback across.

// Server Action
async function archive(id: string) {
  "use server";
  await db.invoice.update({ where: { id }, data: { archived: true } });
}

Stream instead of blocking

RSC's superpower is streaming. Wrap slow data in Suspense and the shell renders instantly while the expensive part fills in:

import { Suspense } from "react";

export default function Page() {
  return (
    <>
      <Header />
      <Suspense fallback={<TableSkeleton />}>
        <SlowInvoiceTable />
      </Suspense>
    </>
  );
}

This kills the all-or-nothing spinner and dramatically improves perceived performance on data-heavy screens.

Fetch close to where you render

Because Server Components run on the server, you can call your database or API directly in the component that needs the data—no prop-drilling, no client waterfall. React dedupes identical requests within a render, so co-locating fetches is safe and keeps components self-contained.

Mistakes to avoid

  • "use client" at the top of a layout. It poisons everything below it.
  • Importing server-only code into a client module. Use the server-only package to fail loudly at build time.
  • Treating Server Actions as a free API. They're mutations with real security implications—validate input and check auth every time.
  • Forgetting the network still exists. Streaming hides latency; it doesn't remove it. Measure.

The takeaway

RSC rewards a simple discipline: render on the server, opt into the client at the leaves, and stream anything slow. Get the boundary right and you ship less JavaScript, fetch data without waterfalls, and hand users a faster app. Get it wrong and you've built a heavier client app with extra steps. The architecture is the feature.

#React#Server Components#Next.js#RSC#Performance
AG

About Ansh

Frontend engineer with 4+ years building scalable SaaS products, design systems, CRM, analytics and omnichannel platforms.

More about me →

Related reading