React Server Components in Next.js 14: Production Patterns That Work
ReactNext.jsFrontend

React Server Components in Next.js 14: Production Patterns That Work

When to use Server Components vs Client Components in Next.js 14, the patterns that survive production, and the foot-guns I keep tripping over after shipping 8+ App Router projects.

HJ
Hassan Javed
April 2026
10 min read

The mental model that finally clicked

I shipped my first Next.js 14 App Router project in early 2024. I shipped my eighth one a few weeks ago. Somewhere between project 3 and project 5, the mental model for Server Components finally clicked. This post is what I'd tell my early-2024 self.

The short version: Server Components are the default. Client Components are an opt-in for interactivity. Most of your tree should be server. Most of your bundle size win comes from keeping it that way.

The two-layer rule

Every page in App Router has two conceptual layers:

1.Server layer — data fetching, content rendering, SEO metadata
2.Client layer — interactivity, browser APIs, state, animation

Your job is to keep these layers as thin as possible. Specifically: don't let client components grow into the server's job, and don't let server components grow into the client's job.

In practice this means: a page that displays a list of products should be a server component. The "Add to Cart" button on each product card is a client component. Not the whole card — just the button.

The pattern: server shell, client islands

The pattern I keep coming back to:

tsxcode
// app/products/page.tsx — server component
import { getProducts } from "@/lib/db";
import AddToCartButton from "./AddToCartButton";

export default async function ProductsPage() {
  const products = await getProducts();
  return (
    <ul>
      {products.map((p) => (
        <li key={p.id}>
          <h3>{p.name}</h3>
          <p>{p.price}</p>
          <AddToCartButton productId={p.id} />
        </li>
      ))}
    </ul>
  );
}

The page is rendered on the server. The product list HTML lands on the page directly — Google indexes it, social previews see it, your LCP is fast. Only the buttons hydrate.

When you actually need a Client Component

Be ruthless. You only need "use client" for:

1.State — useState, useReducer, useContext
2.Effects — useEffect, useLayoutEffect
3.Refs — useRef (for DOM access)
4.Browser-only APIs — window, localStorage, IntersectionObserver
5.Event handlers — onClick, onChange
6.Class components (rare in 2026)
7.Libraries that haven't been updated — Framer Motion's animations, some chart libraries

Most "I need 'use client' for this" instincts are wrong. Test the instinct: would this work as a server component? Often yes.

The data-fetching shift

In Pages Router, data fetching happened in getServerSideProps or getStaticProps. In App Router, it happens directly in the server component. This is a quality-of-life win.

Gotcha: fetch in App Router is automatically deduplicated and cached. That sounds great until you forget you're hitting a stale cache for 24 hours after the data changed. Use { cache: "no-store" } or revalidation tags explicitly.

Common foot-guns

1. Importing client-only libraries into server components

A chart library that uses window inside a server component breaks at build time. Wrap the chart in a client component, then import the wrapper into the server component.

2. Passing functions as props from server to client

Functions can't be serialized across the boundary. Pass primitives or move the handler into the client component.

3. Adding useState to a layout you forgot was a server component

App Router layouts are server by default. Adding useState silently breaks the build (or worse, makes the entire layout client and forces children to hydrate).

4. Hydration mismatches from time-dependent rendering

Rendering new Date() directly mismatches because server and client run at different times. Render time-dependent values inside useEffect or pass them down from the server.

5. Streaming and Suspense — easy to over-use

You don't need everywhere. Use it when you have a slow data fetch you want to defer below a fast-loading shell. For most pages, a single server-side fetch is fine.

The MERN angle

I work across both Web3 (Solidity, Ethers.js) and pure MERN stacks (React, Next.js, Node.js, MongoDB). For MERN clients, Next.js App Router is now my default — fewer moving parts than Express + React + separate backend. The API routes in App Router handle most backend needs.

When that's not enough, I drop in a separate Express service. But honestly, with React Server Actions handling form submissions and database writes directly, even Express is becoming optional for many SaaS apps.

What I'd build today

A typical SaaS dashboard, in App Router, in 2026:

/ — server-rendered marketing page, no client JS at all
/dashboard — server-rendered shell, client islands for interactive widgets
/dashboard/settings — server-rendered form, Server Actions for submissions
/api/webhooks/stripe — route handler in App Router (replaces Express webhook endpoint)
Database: MongoDB or PostgreSQL (Postgres + Drizzle ORM is my current preference)
Auth: NextAuth or Clerk
Deployment: Vercel for the Next app, Mongo Atlas for the DB

The whole thing has maybe 30% as much code as the equivalent CRA + Express setup I'd have built in 2022. That's the App Router win.

TL;DR

Server Components by default, Client Components only when needed (state/effects/handlers/browser APIs)
Pattern: server shell, client islands — keep client components small and leaf-positioned
Watch the fetch cache; default behavior surprises people
App Router has reduced backend complexity for most SaaS — give it a real try before reaching for Express

If you're shipping Next.js 14 work and want a senior reviewer or full-build engagement, contact me — I take on Next.js + MERN projects alongside Web3 work.

Related Reads

You might also like