The Biggest Mental Shift in Next.js
This is THE concept that confuses people coming from React. In Next.js (App Router), components are Server Components by default. They run on the server, never ship JavaScript to the browser, and can directly access databases/file systems.
How it works under the hood (RSC Payload)
⢠A compact binary format of rendered Server Components
⢠Contains placeholders for where Client Components go
⢠Includes props being passed between server and client
The browser uses this to stitch the full page together.
Server Components (Default)
1// This is a Server Component (no "use client" directive)2// It runs ONLY on the server34import { db } from "@/lib/database";56export default async function UsersPage() {7// This runs on the server. The SQL never reaches the browser8const users = await db.query("SELECT * FROM users");910return (11 <ul>12 {users.map((user) => (13 <li key={user.id}>{user.name}</li>14 ))}15 </ul>16);17}Why this matters
Client Components
Need interactivity? State? Event handlers? Browser APIs? Add 'use client' at the top:
1"use client";23import { useState } from "react";45export default function Counter() {6const [count, setCount] = useState(0);78return (9 <div>10 <p>Count: {count}</p>11 <button onClick={() => setCount(count + 1)}>12 Increment13 </button>14 </div>15);16}When to Use Which?
The Composition Pattern
The trick is: keep most things as Server Components and sprinkle Client Components only where needed:
1// src/app/dashboard/page.tsx (Server Component)2import { db } from "@/lib/database";3import { LikeButton } from "@/components/LikeButton"; // Client45export default async function Dashboard() {6const posts = await db.query("SELECT * FROM posts");78return (9 <div>10 <h1>Dashboard</h1>11 {posts.map((post) => (12 <article key={post.id}>13 <h2>{post.title}</h2>14 <p>{post.body}</p>15 {/* Only this small button is a Client Component */}16 <LikeButton postId={post.id} />17 </article>18 ))}19 </div>20);21}My rule of thumb
⢠You need useState or useEffect
⢠You need onClick, onChange, or other event handlers
⢠You need browser APIs (localStorage, window)
⢠You need custom hooks
Push client boundaries as low as possible in your component tree.
Context Providers Pattern
React Context doesn't work in Server Components. But you still need things like theme providers. The trick: make the provider a Client Component, import it in your layout (Server Component), and pass children through it:
1// src/providers/theme-provider.tsx2"use client";34import { createContext } from "react";56export const ThemeContext = createContext({});78export default function ThemeProvider({9children,10}: {11children: React.ReactNode;12}) {13return (14 <ThemeContext.Provider value="dark">15 {children}16 </ThemeContext.Provider>17);18}1// src/app/layout.tsx (Server Component!)2import ThemeProvider from "@/providers/theme-provider";34export default function RootLayout({ children }) {5return (6 <html>7 <body>8 <ThemeProvider>{children}</ThemeProvider>9 </body>10 </html>11);12}Server Components that are passed as children render on the server first, then get slotted into the Client Component. Best of both worlds.
Wrapping Third-Party Components
Some npm packages use client-only features but don't have 'use client' in their code. You'll get an error using them in Server Components. The fix is dead simple:
1// src/components/carousel.tsx2"use client";34// Just re-export with the directive5import { Carousel } from "acme-carousel";6export default Carousel;78// Now you can use <Carousel /> in any Server Component