Understanding 'use client' and 'use server' in Next.js


My first App Router mistake was putting 'use client' at the top of a layout because one button needed onClick. The whole subtree — including data fetching I thought was "server" — shipped JavaScript I did not need.
Next.js defaults to Server Components. 'use client' is the escape hatch. 'use server' marks functions that run on the server but can be invoked from the client.
This post is how I draw those lines now. Pair with What's New in Next.js 15 for caching context and JWT vs sessions when auth cookies meet Server Components.
import { db } from "@/lib/db";
export default async function BlogPage() {
const posts = await db.post.findMany();
return (
<div>
{posts.map((post) => (
<div key={post.id}>{post.title}</div>
))}
</div>
);
}
What you get:
What you cannot do: useState, useEffect, onClick, window.
Only when the component needs:
window, localStorage, observersonClick, onChange"use client";
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount((c) => c + 1)}>Count: {count}</button>;
}
Rule I follow: Push 'use client' as far down the tree as possible — wrap the interactive leaf, not the page.
Server Actions — async functions that run on the server, called from forms or client code.
// app/actions.ts
"use server";
import { db } from "@/lib/db";
import { revalidatePath } from "next/cache";
export async function createPost(formData: FormData) {
const title = formData.get("title") as string;
const content = formData.get("content") as string;
await db.post.create({ data: { title, content } });
revalidatePath("/posts");
}
"use client";
import { createPost } from "@/app/actions";
export default function PostForm() {
return (
<form action={createPost}>
<input name="title" />
<textarea name="content" />
<button type="submit">Create</button>
</form>
);
}
Why: No hand-rolled API route for every mutation if the action fits.
// Bad — whole page client because of one button
"use client";
import { db } from "@/lib/db"; // does not work — server-only
export default function Page() {
return <InteractiveButton />;
}
// Good — server page, client leaf
import InteractiveButton from "./InteractiveButton";
export default async function Page() {
const data = await db.query();
return (
<div>
<h1>{data.title}</h1>
<InteractiveButton />
</div>
);
}
Lesson: 'use client' is contagious to imports. Minimize the infected subtree.
'use server')Unnecessary 'use client' on static markup — costs bundle for zero benefit.
Browser APIs in Server Components — window is not defined. Split the component.
Giant client providers at root — theme OK; entire app state tree not OK if avoidable.
| Server Component | Client Component | |
|---|---|---|
| JS to browser | None (this component) | Bundled |
| Data access | Direct | Via fetch/actions |
| Interactivity | No | Yes |
Server Actions add server round-trips — still often simpler than REST boilerplate for forms.
Open your largest 'use client' file. Ask: does this file need the browser, or only a child? Split.
Default server. Add client at the leaves. Use Server Actions for mutations. Re-read React 19 Actions when you want pending states without manual wiring.
That is most of App Router performance and security in one habit: small client islands.