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 type | Runs on | Perfect for | Remember it like |
|---|---|---|---|
| Server Component | Node.js runtime / Edge | Data fetching, heavy logic, accessing secrets | 👩🍳 Kitchen prep done behind the counter |
| Client Component | Browser | Interactivity, event handlers, local state | 🕹️ Game controller in the visitor’s hands |
| Hybrid tree | Both | Real 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
/typesor/libso 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.tsxor 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? 🧭
| Question | Choose |
|---|---|
| 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
/apiroute that proxies a server fetch.
Further reading 📚
- Jordan’s guide — https://dev.to/devjordan/nextjs-15-app-router-complete-guide-to-server-and-client-components-5h6k
- Charles Arnaud on streaming — https://dev.to/charnog/nextjs-how-and-components-streaming-works-30ao
- Next.js docs — https://nextjs.org/docs/app/building-your-application/rendering/composition-patterns