Step 2
Step 2 — Server vs Client components
0 views
Step 2 — Server vs Client components
The core App Router concept. Server Component by default, opt in to 'use client'.
1. Why split?
- Most code doesn't need to ship to the browser
- Server fetches DB/APIs, sends only HTML
- Smaller client bundle, faster initial load
- Secrets (API keys) stay on the server
2. Server Component (default)
async function getPosts() {
const res = await fetch("https://api.example.com/posts", {
next: { revalidate: 60 },
});
return res.json();
}
export default async function PostsPage() {
const posts = await getPosts();
return (
<ul className="space-y-2 p-6">
{posts.map((p: any) => <li key={p.id}>{p.title}</li>)}
</ul>
);
}
asynccomponents allowedfetchresults cached withrevalidate- No
useState, no event handlers - No
document/window
3. Client Component
"use client";
import { useState } from "react";
export default function Counter() {
const [n, setN] = useState(0);
return (
<button onClick={() => setN(n + 1)}
className="rounded bg-blue-600 px-4 py-2 text-white">
{n}
</button>
);
}
'use client' at the very top.
4. When to use 'use client'
useState,useEffect,useRef- Event handlers (
onClick,onChange) - Browser APIs (
window,localStorage) - Browser-only libraries
5. Composing
import Counter from "./counter";
async function getUser() { return { name: "Alice" }; }
export default async function Home() {
const user = await getUser();
return (
<main className="p-6 space-y-4">
<h1 className="text-2xl">Hello, {user.name}</h1>
<Counter />
</main>
);
}
Server imports Client. The reverse (Client importing Server) is indirect — pass Server components as children.
6. Props are serializable
<Counter initial={42} />
Only JSON-serializable values cross the boundary — no functions, no class instances.
7. Keep the boundary shallow
Bad: 'use client' at the root → everything client.
Good: keep 'use client' scoped to interactive leaf components.
8. cookies() · headers()
Server-only.
import { cookies, headers } from "next/headers";
export default async function Page() {
const cookieStore = await cookies();
const theme = cookieStore.get("theme")?.value ?? "light";
return <div>theme: {theme}</div>;
}
Next 15+ returns Promises.
9. Server Action — form submit
async function createPost(formData: FormData) {
"use server";
const title = formData.get("title") as string;
// save to DB
}
export default function NewPostPage() {
return (
<form action={createPost} className="p-6 space-y-2">
<input name="title" className="border p-2" />
<button type="submit" className="bg-blue-600 text-white px-4 py-2">Save</button>
</form>
);
}
No API route needed.
10. Streaming + Suspense
import { Suspense } from "react";
import PostList from "./post-list";
export default function Page() {
return (
<main>
<h1>Posts</h1>
<Suspense fallback={<p>loading...</p>}>
<PostList />
</Suspense>
</main>
);
}
11. Cache / revalidate
fetch(url, { next: { revalidate: 3600 } });
fetch(url, { cache: "no-store" });
fetch(url, { next: { tags: ["posts"] } });
After mutation: revalidateTag("posts").
12. force-dynamic
export const dynamic = "force-dynamic";
Bypasses caching. Use only when real-time data is essential.
13. Gotchas
useStatewithout'use client'— build errorwindowin Server Component — runtime error- Non-serializable props
- DB access from Client — use Server Actions or API routes
cookies()in Client — server-only
Closing
Server by default, Client by opt-in. That one sentence summarizes App Router thinking.
Next
- 03-api-drizzle
References: Next.js — Server and Client Components · React 19 Server Components.