React

React Server Components Explained: What Actually Changes for Your Team

PN
Priya Nathan
CTO & Co-Founder · · 7 min read

Whenever a client asks us whether they should adopt React Server Components, the conversation that follows is rarely about React at all. It's about how their team currently makes decisions about where code runs — and how much of that decision-making has been implicit, inherited, or simply never revisited since the app was scaffolded. RSCs don't just change your bundle size. They force a decision your team has probably been avoiding.

The default your team didn't know it had

In a conventional client-rendered React app, every component is a client component by default, whether or not it needs to be. Data gets fetched in useEffect, state gets threaded through context or a state library, and the entire component tree ships as JavaScript to the browser regardless of whether most of it ever re-renders on the client. Nobody chose this — it's just what "a React component" has meant since the framework's early days.

Server Components invert that default. A component is a server component — rendered once on the server, sending only its output HTML/serialized payload to the browser — unless it's explicitly marked with 'use client'. That single inversion is the entire technical premise, and it has a much larger blast radius than it sounds like.

// Runs only on the server. Ships zero JavaScript to the browser for this component.
async function OrderHistory({ userId }: { userId: string }) {
  const orders = await db.order.findMany({ where: { userId } });
  return <OrderTable orders={orders} />;
}

What actually needs 'use client'

The single most common mistake we see in early adoptions — including our own first internal migration — is marking far more components as client components than necessary, out of habit or caution. A component genuinely needs the client boundary only if it does one of the following:

  • Holds local state or uses lifecycle hooks (useState, useEffect, useReducer)
  • Attaches a browser event handler (onClick, onChange, onScroll)
  • Reads a browser-only API (window, localStorage, IntersectionObserver)
  • Depends on a third-party library that assumes it's running in a browser

Everything else — layout shells, most presentational design-system components, static marketing sections, data-fetching wrappers — can stay server-rendered. When we audit client codebases component-by-component instead of converting entire page trees wholesale, it's common to find 60-70% need no client directive at all.

The composition pattern that resolves most objections

The part that trips teams up isn't the client/server split itself — it's realizing that a client component can't import a server component directly, but it can receive one as children or a prop. That lets you keep an interactive shell (a modal, an expandable panel, a tab switcher) on the client while still rendering server-fetched content inside it:

'use client';
function ExpandablePanel({ children }: { children: React.ReactNode }) {
  const [open, setOpen] = useState(false);
  return (
    <div>
      <button onClick={() => setOpen(!open)}>Toggle</button>
      {open && children}
    </div>
  );
}

// Rendered from a server component:
<ExpandablePanel>
  <OrderHistory userId={user.id} />
</ExpandablePanel>

Once engineers internalize this pattern, most of the "but I need interactivity around server-fetched data" objections resolve on their own.

Data fetching gets simpler, then introduces a new failure mode

Fetching directly inside server components removes a whole category of isLoading / error / data boilerplate that's been copy-pasted across React codebases for a decade. But it introduces a subtler problem: waterfalls. If a parent component awaits a fetch, and its child independently awaits its own fetch, they run sequentially by default unless you explicitly parallelize with Promise.all or lift the fetches to a shared ancestor. This is the single most common performance regression teams introduce right after adopting RSCs, and it's dangerous precisely because it still "just works" — it's only slower, and slower doesn't throw an error in code review.

What actually changes for the team, not just the code

The technical mechanics matter less than the organizational shift they force:

  1. Code review changes shape. Reviewers now need to ask "does this need to be a client component?" as a default question, the same way they'd ask about test coverage. Teams that skip this review step drift back toward the old default within a few months.
  2. The frontend/backend line gets blurrier. Server components that query a database directly mean frontend engineers are writing code that used to live behind an API boundary. Teams with a strict frontend/backend split need an explicit conversation about where that boundary now sits, or ownership gets confused fast.
  3. Bundle size becomes a design conversation, not just a build metric. Because the client/server boundary is chosen per component, product and engineering can have a much more granular conversation about "does this feature need to be interactive" earlier in design, instead of discovering the bundle cost after ship.

When it's worth the migration cost

RSCs aren't free to adopt. They require a framework that supports the model well — Next.js App Router is the mature option today — and a genuine mental-model shift for engineers who've built exclusively client-rendered apps for years. For content- and data-heavy applications — dashboards, admin tools, marketplaces, internal line-of-business apps — the payoff is substantial: smaller client bundles, faster time-to-interactive, and an entire class of client/server data-fetching bugs that simply stops occurring. For highly interactive, canvas-style products — design tools, real-time collaborative editors — the benefit is smaller relative to the migration cost, and we usually advise those teams to wait or adopt selectively.

On a recent client migration of a mid-size internal dashboard, the client-shipped JavaScript dropped by more than half, but the number our client actually cared about six months later wasn't the bundle size — it was that a whole category of "why is this data stale on the client" bug reports had simply stopped showing up in their issue tracker. That's the change worth planning for, and it's the one most teams don't fully appreciate until they've lived with it for a quarter.

#React#Server Components#Next.js#frontend architecture
Keep Reading

More from the blog

Let's build something reliable, together.

Tell us about your project and get a free technical consultation within one business day — no obligation, no sales pressure.