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.
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:
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:
// 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:
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
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)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
fetch cache; default behavior surprises peopleIf 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.
You might also like
Next.js App Router vs Pages Router: Lessons From 12 Project Migrations
What changes, what breaks, what to migrate first, and what to leave alone. Hard-earned migration lessons from moving 12 client projects from Pages Router to App Router.
Modern CSS in 2026: Container Queries, :has(), and Tailwind v4
Container queries, :has(), cascade layers, color-mix() — the CSS features I'm actually using on production projects in 2026 and how they change the way I structure components.
How to Hire a Blockchain Developer in Pakistan in 2026
A practical guide to hiring blockchain developers from Pakistan in 2026 — where to find them, what rates to expect, red flags, contract structure, and how to evaluate Solidity skill in 30 minutes.