Appearance
Server Components vs Client Components
Default in App Router: server. You opt into the browser with
'use client'. Feels backwards if you just learned hooks - until the bundle size drops and it clicks.
The default: Server Component
tsx
// app/posts/page.tsx
async function getPosts() {
const res = await fetch('https://jsonplaceholder.typicode.com/posts', {
next: { revalidate: 60 },
})
return res.json()
}
export default async function PostsPage() {
const posts = await getPosts()
return (
<ul>
{posts.slice(0, 5).map((p: { id: number; title: string }) => (
<li key={p.id}>{p.title}</li>
))}
</ul>
)
}Comments for the code above:
asyncpage component - data loads on the server- No
useEffect, no loading spinner state unless you wantloading.tsx - This code does not need
'use client'- and shouldn't have it
When you need the client
Hooks (useState, useEffect), browser APIs, event handlers that aren't just <form action={…}> - you need a Client Component:
tsx
// components/LikeButton.tsx
'use client'
import { useState } from 'react'
export function LikeButton() {
const [likes, setLikes] = useState(0)
return (
<button type="button" onClick={() => setLikes((n) => n + 1)}>
Likes: {likes}
</button>
)
}tsx
// app/posts/page.tsx (Server Component)
import { LikeButton } from '@/components/LikeButton'
export default async function PostsPage() {
const posts = await getPosts()
return (
<div>
<PostList posts={posts} />
<LikeButton />
</div>
)
}As you can see - keep the page on the server, drop a small interactive island in. Nuxt islands / client components itch the same spot.
Rules that save you pain
'use client'marks a boundary - that file and its imports become a client bundle (transitive)- You can import a Client Component into a Server Component
- You cannot import a Server Component into a Client Component (pass as
childreninstead) - Don't slap
'use client'on the root layout "just to make hooks work" - push it down
Pass children through the boundary
tsx
// ClientShell.tsx
'use client'
export function ClientShell({ children }: { children: React.ReactNode }) {
return <div className="shell">{children}</div>
}
// Server page
<ClientShell>
<ExpensiveServerStuff />
</ClientShell>children can still be rendered on the server and passed in. Handy pattern.
Vue brain check
| Instinct | Next reality |
|---|---|
| Everything is a Vue SFC that hydrates | Only client components hydrate |
useFetch in setup everywhere | Prefer server fetch in RSC |
| Pinia for server data | Often cache on server + Query on client islands |
Next: caching / revalidation without mythology.
Best,
-- ł.