Skip to content
C.W.K.
Stream
Lesson 04 of 08 · published

The Decision Framework

~18 min · server vs client, decision, tree

Level 0Curious
0 XP0/68 lessons0/11 achievements
0/120 XP to next level120 XP to go0% complete

The default is Server. Promote on hard requirement only.

Walk every component you write through this tree:

Does it need...
├── useState / useEffect / useReducer?  → Client
├── onClick / onChange / onSubmit?      → Client
├── Browser APIs (window, navigator)?   → Client
├── A library that requires hooks?      → Client
└── None of the above?                  → Server ✓

What "mostly server" looks like

In a typical Next.js app, 80–90% of components are Server Components. Pages, lists, cards, headers, footers, articles, tables — all server. Only the leaves that hold state or event handlers wear 'use client'.

Real examples

  • Blog post page (renders content) — Server.
  • Search bar (controlled input) — Client.
  • Markdown renderer (data shape) — Server.
  • Theme toggle (writes to localStorage) — Client.
  • Like button (optimistic UI) — Client wrapper, server data feed.

Use browser-only requirements as the decision tree: state, effects, events, browser APIs, or hook-based libraries require a client boundary. Pure display, data access, secret access, and heavy shaping remain server work. This produces a server-heavy tree as an outcome, not a quota. An editor or canvas may legitimately be client-heavy. Chasing an 80–90% server ratio can create more network chatter and awkward state than it saves. Optimize for the workload and security boundary, not for a fashionable percentage.

Annotate ten components with the exact requirement that determines their side, then compare those notes with current directives. Move one unjustified boundary and verify bundle size, initial HTML, and interaction rather than declaring success from the file count.

Code

Server: blog page just renders·tsx
// app/blog/[slug]/page.tsx — Server
export default async function BlogPost({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const post = await fetchPost(slug);
  return <article>{post.content}</article>;
}
Client: search bar reads/writes state·tsx
// components/SearchBar.tsx — Client
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';

export function SearchBar() {
  const [q, setQ] = useState('');
  const router = useRouter();
  return (
    <form onSubmit={(e) => { e.preventDefault(); router.push(`/search?q=${encodeURIComponent(q)}`); }}>
      <input value={q} onChange={(e) => setQ(e.target.value)} placeholder="Search" />
    </form>
  );
}

External links

Exercise

Audit any 10 components in your codebase. For each, decide Server or Client by the framework above. Flip any that don't actually need 'use client'.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.