A simple decision tree: start as a Server Component. Only switch when you need something a Server Component can't do.
useState, useReduceruseEffect, useLayoutEffectonClick, onChange, onSubmitwindow, document, localStorage, navigator, IntersectionObserveruseContext (the Provider itself must be a Client Component)"If I deleted JavaScript on the client entirely, would this still work?"
If yes — keep it server. If no — make it client.
1// app/contact/page.tsx — Server Component
2import { submitContact } from "./actions";
3
4export default function ContactPage() {
5 return (
6 <form action={submitContact}>
7 <input name="email" type="email" required />
8 <textarea name="message" required />
9 <button>Send</button>
10 </form>
11 );
12}No "use client" needed — the form posts to a Server Action. Works without JavaScript.
1// components/ThemeToggle.tsx
2"use client";
3import { useState, useEffect } from "react";
4
5export function ThemeToggle() {
6 const [dark, setDark] = useState(false);
7 useEffect(() => {
8 document.documentElement.classList.toggle("dark", dark);
9 }, [dark]);
10 return <button onClick={() => setDark((d) => !d)}>{dark ? "☀️" : "🌙"}</button>;
11}Must be client — needs state and DOM access.
1// Server — reads cookies on the server
2export default async function Header() {
3 const user = await getCurrentUser();
4 return (
5 <header>
6 <Logo />
7 {user ? <UserMenu user={user} /> : <SignInButton />}
8 </header>
9 );
10}Don't ship auth logic to the client — read the session server-side.
1// app/layout.tsx
2"use client"; // ❌ Now the entire app is a client component treeDefaults to client → loses Server Component benefits everywhere. Push "use client" to the leaves.
The third-party providers (Theme, ReactQuery, Redux) are all Client Components. Wrap them in a single Providers file and use it inside the server root layout:
1// components/Providers.tsx
2"use client";
3import { ThemeProvider } from "next-themes";
4import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
5
6const qc = new QueryClient();
7export function Providers({ children }: { children: React.ReactNode }) {
8 return (
9 <QueryClientProvider client={qc}>
10 <ThemeProvider>{children}</ThemeProvider>
11 </QueryClientProvider>
12 );
13}
14
15// app/layout.tsx — Server Component
16import { Providers } from "@/components/Providers";
17
18export default function RootLayout({ children }: { children: React.ReactNode }) {
19 return (
20 <html lang="en"><body><Providers>{children}</Providers></body></html>
21 );
22}