Skip to Content
NextjsServer & Client Components

Next.js Components Playbook 🌐

Next.js 15 leans on the App Router to mix Server Components (default) and Client Components ("use client"). This cheat sheet distills ideas from Jordan’s deep dive  into plain reminders, code you can copy, and sticky analogies.

Quick map 🗺️

Component typeRuns onPerfect forRemember it like
Server ComponentNode.js runtime / EdgeData fetching, heavy logic, accessing secrets👩‍🍳 Kitchen prep done behind the counter
Client ComponentBrowserInteractivity, event handlers, local state🕹️ Game controller in the visitor’s hands
Hybrid treeBothReal apps—server shells + client islands🏝️ Static resort island with lively beach spots

Analogy: Let the server kitchen cook the meal, and hand out client utensils only where guests need to stir or slice it themselves.

Server Components 🍳

  • 🧠 Default when no "use client" pragma is present.
  • 🔐 Can access environment variables, databases, and secrets directly.
  • 🧊 Never ship JS to the browser for that component’s logic.
// app/posts/page.tsx import { Suspense } from 'react' import PostsList from './posts-list' export default async function PostsPage() { const user = await auth() return ( <main> <h1>Welcome, {user.name}</h1> <Suspense fallback={<p>Loading posts…</p>}> <PostsList /> </Suspense> </main> ) } async function PostsList() { const posts = await fetch('https://api.example.com/posts', { next: { revalidate: 120 }, }).then((res) => res.json()) return ( <ul> {posts.map((post) => ( <li key={post.id}>{post.title}</li> ))} </ul> ) }

📦 Memory trick: Server Components are like pre-packed lunch trays—fully assembled before leaving the kitchen.

Client Components 🕹️

  • 🧩 Opt-in by adding "use client" at the top of the file.
  • 🎛️ Needed whenever you use state, refs, effects, or browser-only APIs (localStorage, window).
  • 📦 Can receive props from Server Components but cannot import server-only modules.
// app/posts/filter-panel.tsx 'use client' import { useState } from 'react' export default function FilterPanel({ initialTag }: { initialTag: string }) { const [tag, setTag] = useState(initialTag) return ( <section> <label htmlFor="tag">Filter by tag</label> <select id="tag" value={tag} onChange={(event) => setTag(event.target.value)} > <option value="all">All</option> <option value="nextjs">Next.js</option> <option value="react">React</option> </select> <p>Showing: {tag}</p> </section> ) }

🎨 Analogy: Client Components are art stations where visitors can paint or rearrange pieces themselves.

Mixing both 🔗

The most common pattern is server shell + client islands: render data-heavy sections on the server, then pass props to client widgets.

// app/posts/page.tsx import FilterPanel from './filter-panel' import PostsList from './posts-list' export default async function PostsPage() { const tags = await getPopularTags() const posts = await getInitialPosts() return ( <main> <FilterPanel initialTag={tags[0]} /> <PostsList initialPosts={posts} /> </main> ) }

Rules of thumb:

  • 📤 Props flow from server → client. Passing functions upward is not allowed.
  • 📚 Keep shared types in /types or /lib so both sides import the same definitions.

🪡 Analogy: The server stitches the quilt, then hands it to the client to add interactive patches.

Streaming & Suspense 💧

  • ⏱️ Use <Suspense> boundaries in Server Components to progressively stream HTML.
  • 🪄 Pair with loading.tsx or inline fallbacks for skeletons.
  • 🛰️ Ideal when part of the page waits on slow data but the rest should ship immediately.
  • 📡 Behind the scenes (per Charles Arnaud’s explainer ), React serializes chunks of the component tree into Flight data, ships them over the network, and hydrates matching client islands as soon as their payload arrives.
  • 🧵 You can nest Suspense boundaries per route segment so that headers, sidebars, and critical UI ship instantly while slower widgets stream in later.
// app/dashboard/page.tsx import { Suspense } from 'react' import RevenueChart from './revenue-chart' export default function Dashboard() { return ( <main> <h1>Dashboard</h1> <Suspense fallback={<p>Gathering revenue data…</p>}> <RevenueChart /> </Suspense> </main> ) }

🌊 Analogy: Suspense is a curtain—you open parts of the stage when each actor is ready, and the backstage crew (React Server Components) radios in each performer as soon as their costume fits.

When to reach for what? 🧭

QuestionChoose
Do you need secrets, direct DB calls, or zero JS?Server Component
Do you need clicks, drag-and-drop, or browser APIs?Client Component
Do you need both?Server parent rendering client children
Is initial load slow due to API latency?Add Suspense + streaming

Handy reminders 🧠

  • 🧹 Keep Client Components as small islands; pull data fetching up to the nearest Server Component.
  • 🧭 Co-locate files: page.tsx (server) + widget.tsx (client) within the same segment for clarity.
  • 🧪 Test client pieces separately with Storybook/Playwright, test server pieces via integration tests or route handlers.
  • 🔄 If a Client Component needs fresh data, expose a tiny /api route that proxies a server fetch.

Further reading 📚

Last updated on