2026/06/26
開始使用 Next.js App Router 之後,最常遇到的問題之一就是:
這個元件到底要不要加 'use client'?
一開始很容易看到錯誤就直接加。
元件不能用 `useState`,加。
不能用 `useEffect`,加。
不能用 click event,加。
第三方套件報錯,也加。
最後整個頁面越來越多 Client Component,原本 App Router 想帶來的 Server Component 優勢,可能就慢慢消失了。
我自己現在會把這件事想成:
預設先 Server,需要互動再 Client。
這句話很簡單,但實務上難的是判斷「什麼叫需要互動」。
---
Server Component 是在伺服器端執行的元件。
它適合處理:
例如文章詳情頁:
1export default async function PostPage({
2 params,
3}: {
4 params: Promise<{ slug: string }>;
5}) {
6 const { slug } = await params;
7
8 const post = await getPostBySlug(slug);
9
10 return (
11 <main>
12 <h1>{post.title}</h1>
13 <article>{post.content}</article>
14 </main>
15 );
16}這種頁面本身不一定需要互動。
它只是根據 `slug` 取得文章資料,然後渲染內容。
這時候讓它保持 Server Component 會比較合理。
---
Client Component 是會送到瀏覽器端執行的元件。
只要元件需要使用 React client-side feature,就要加 'use client'。
例如:
1'use client';
2
3import { useState } from 'react';
4
5export function Counter() {
6 const [count, setCount] = useState(0);
7
8 return (
9 <button onClick={() => setCount((prev) => prev + 1)}>
10 Count: {count}
11 </button>
12 );
13}只要有這些需求,通常就是 Client Component:
| 需求 | 原因 |
|---|---|
| `useState` | 需要瀏覽器端狀態 |
| `useEffect` | 需要在瀏覽器端執行副作用 |
| `onClick` | 需要使用者互動 |
| `window` / `document` | 只能在瀏覽器端使用 |
| `localStorage` | 瀏覽器 API |
| Zustand | client state |
| React Query | client-side cache |
| 表單即時互動 | 需要瀏覽器狀態 |
---
這是最常見的情況。
例如:
1'use client';
2
3export default function PostPage() {
4 return (
5 <main>
6 <PostHeader />
7 <PostContent />
8 <LikeButton />
9 </main>
10 );
11}這樣不是不能跑,但問題是整個 page 都變成 Client Component。
如果 `PostHeader`、`PostContent` 其實只是靜態內容,它們也會被一起拉到 client side。
比較好的切法是:
1export default async function PostPage() {
2 const post = await getPost();
3
4 return (
5 <main>
6 <PostHeader title={post.title} />
7 <PostContent content={post.content} />
8 <LikeButton postId={post.id} />
9 </main>
10 );
11}然後只有真的需要互動的部分加 'use client':
1'use client';
2
3export function LikeButton({ postId }: { postId: string }) {
4 return <button onClick={() => console.log(postId)}>Like</button>;
5}這樣頁面主體仍然是 Server Component。
只有 `LikeButton` 是 Client Component。
---
很多時候,我們只是需要某個小按鈕有互動。
但如果沒有切好,就會讓整個區塊變成 Client Component。
例如:
1'use client';
2
3export function PostCard({ post }: { post: Post }) {
4 return (
5 <article>
6 <h2>{post.title}</h2>
7 <p>{post.excerpt}</p>
8 <button onClick={() => alert('saved')}>收藏</button>
9 </article>
10 );
11}這樣 `PostCard` 會整個變 Client。
如果文章卡片內容很多,而且大部分都是靜態顯示,可以改成:
1export function PostCard({ post }: { post: Post }) {
2 return (
3 <article>
4 <h2>{post.title}</h2>
5 <p>{post.excerpt}</p>
6 <SaveButton postId={post.id} />
7 </article>
8 );
9}1'use client';
2
3export function SaveButton({ postId }: { postId: string }) {
4 return (
5 <button onClick={() => alert(`saved: ${postId}`)}>
6 收藏
7 </button>
8 );
9}這樣 Client Component 的範圍就比較小。
---
以前寫 React SPA 很習慣這樣:
1'use client';
2
3import { useEffect, useState } from 'react';
4
5export default function PostsPage() {
6 const [posts, setPosts] = useState<Post[]>([]);
7
8 useEffect(() => {
9 fetch('/api/posts')
10 .then((res) => res.json())
11 .then(setPosts);
12 }, []);
13
14 return (
15 <main>
16 {posts.map((post) => (
17 <PostCard key={post.id} post={post} />
18 ))}
19 </main>
20 );
21}這在傳統 React 裡很常見。
但在 App Router 裡,如果這些資料是頁面初始就需要的內容,通常可以直接在 Server Component 取得:
1export default async function PostsPage() {
2 const posts = await getPosts();
3
4 return (
5 <main>
6 {posts.map((post) => (
7 <PostCard key={post.id} post={post} />
8 ))}
9 </main>
10 );
11}這樣有幾個好處:
如果是部落格文章、商品詳情、行銷頁內容,通常都很適合放在 Server Component。
---
Client Component 會送到瀏覽器。
所以不該把敏感邏輯放進 Client Component。
例如:
1'use client';
2
3const apiKey = process.env.SECRET_API_KEY;這種概念上就不對。
敏感資料、server-only token、資料庫查詢,都應該放在 server 端。
比較好的方式是:
1export default async function DashboardPage() {
2 const data = await getDashboardData();
3
4 return <DashboardView data={data} />;
5}如果畫面需要互動,再把安全處理過的資料傳給 Client Component。
1'use client';
2
3export function DashboardView({ data }: { data: DashboardData }) {
4 return <div>{data.title}</div>;
5}重點是:
Client Component 只拿它需要顯示或互動的資料,不要拿敏感資訊。
---
Server Component 可以 import Client Component。
但傳給 Client Component 的 props 需要是可以序列化的資料。
通常可以傳:
不適合傳:
例如這樣就不適合:
1<ClientComponent onClick={handleClick} />因為 Server Component 裡的 function 不能直接傳到 Client Component。
比較好的方式是讓 Client Component 自己處理互動:
1<ClientComponent postId={post.id} />1'use client';
2
3export function ClientComponent({ postId }: { postId: string }) {
4 function handleClick() {
5 console.log(postId);
6 }
7
8 return <button onClick={handleClick}>Click</button>;
9}Server 負責給資料。
Client 負責互動。
---
很多 UI library 的元件本身需要 client-side 行為。
例如:
這些元件通常會用到 state、effect、DOM event,所以多半需要 Client Component。
但不代表整個頁面都要變 Client。
可以把 UI 套件集中包在小型 Client Component 裡。
例如:
1export default async function SettingsPage() {
2 const profile = await getProfile();
3
4 return (
5 <main>
6 <h1>設定</h1>
7 <ProfileForm profile={profile} />
8 </main>
9 );
10}1'use client';
2
3export function ProfileForm({ profile }: { profile: Profile }) {
4 return (
5 <form>
6 <input defaultValue={profile.name} />
7 <button type="submit">儲存</button>
8 </form>
9 );
10}這樣頁面的資料取得仍然在 Server。
表單互動才放 Client。
---
實務上我會用幾個問題判斷。
---
如果有 click、input、select、drag、keyboard event,通常是 Client Component。
例如:
| 元件 | 建議 |
|---|---|
| LikeButton | Client |
| SearchInput | Client |
| LoginForm | Client |
| ThemeToggle | Client |
| MobileDrawer | Client |
| ArticleContent | Server |
| Breadcrumb | Server |
| Footer | Server |
---
如果需要這些 hooks,就要是 Client Component:
但這裡要注意,不是看到要用 hooks 就直接把整頁改成 Client。
更好的做法是把需要 hooks 的部分拆出去。
---
如果資料是頁面一進來就要看到的內容,通常適合在 Server Component 取得。
例如:
| 資料 | 建議 |
|---|---|
| 文章詳情 | Server |
| 商品詳情 | Server |
| 分類列表 | Server |
| SEO metadata | Server |
| 登入後使用者基本資料 | 視情況 |
| 即時搜尋結果 | Client 或 URL + Server |
| 無限滾動資料 | Client / Hybrid |
---
如果邏輯涉及:
就不要放在 Client Component。
這些應該放在:
Client Component 只負責顯示結果或觸發動作。
---
如果狀態只跟瀏覽器互動有關,通常就是 Client。
例如:
| 狀態 | 建議 |
|---|---|
| modal open | Client |
| tab active | Client |
| drawer open | Client |
| input value | Client |
| localStorage theme | Client |
| selected row | Client |
| URL search params | 視需求 |
| 文章內容 | Server |
---
以部落格文章頁來說,我會這樣切:
1PostPage.server
2 ├── PostHeader.server
3 ├── PostContent.server
4 ├── RelatedPosts.server
5 ├── ShareButton.client
6 └── FavoriteButton.client文章內容、標題、相關文章都可以是 Server Component。
分享按鈕、收藏按鈕需要互動,所以拆成 Client Component。
範例:
1export default async function PostPage({
2 params,
3}: {
4 params: Promise<{ slug: string }>;
5}) {
6 const { slug } = await params;
7 const post = await getPostBySlug(slug);
8
9 return (
10 <main>
11 <PostHeader title={post.title} publishedAt={post.publishedAt} />
12 <PostContent content={post.content} />
13
14 <div>
15 <ShareButton title={post.title} />
16 <FavoriteButton postId={post.id} />
17 </div>
18 </main>
19 );
20}這樣整個頁面不會因為兩個按鈕,就全部變成 Client Component。
---
搜尋頁比較容易卡住。
因為它同時有:
我會先看需求。
如果搜尋結果需要 SEO,或希望網址可以分享:
1/posts?keyword=react&category=frontend&page=1那我會讓 search params 驅動 Server Component 重新取得資料。
1export default async function PostsPage({
2 searchParams,
3}: {
4 searchParams: Promise<{
5 keyword?: string;
6 category?: string;
7 page?: string;
8 }>;
9}) {
10 const params = await searchParams;
11
12 const posts = await getPosts({
13 keyword: params.keyword,
14 category: params.category,
15 page: Number(params.page ?? 1),
16 });
17
18 return (
19 <main>
20 <PostSearchBar />
21 <PostList posts={posts} />
22 </main>
23 );
24}`PostSearchBar` 可以是 Client Component,負責更新 URL。
1'use client';
2
3import { useRouter, useSearchParams } from 'next/navigation';
4
5export function PostSearchBar() {
6 const router = useRouter();
7 const searchParams = useSearchParams();
8
9 function handleSearch(keyword: string) {
10 const params = new URLSearchParams(searchParams);
11
12 if (keyword) {
13 params.set('keyword', keyword);
14 } else {
15 params.delete('keyword');
16 }
17
18 router.push(`/posts?${params.toString()}`);
19 }
20
21 return (
22 <input
23 placeholder="搜尋文章"
24 onChange={(event) => handleSearch(event.target.value)}
25 />
26 );
27}這種切法的概念是:
1Client 更新 URL
2 ↓
3Server 根據 searchParams 取得資料
4 ↓
5畫面重新渲染這樣搜尋條件可以被網址記住,也比較適合列表頁。
---
React Query 是 Client Side 的資料管理工具。
所以只要使用 `useQuery`,那個元件就要是 Client Component。
例如:
1'use client';
2
3import { useQuery } from '@tanstack/react-query';
4
5export function NotificationList() {
6 const { data, isLoading } = useQuery({
7 queryKey: ['notifications'],
8 queryFn: fetchNotifications,
9 });
10
11 if (isLoading) return <p>Loading...</p>;
12
13 return (
14 <ul>
15 {data?.map((item) => (
16 <li key={item.id}>{item.title}</li>
17 ))}
18 </ul>
19 );
20}那什麼時候適合用 React Query?
我通常會放在這些情境:
| 情境 | 適合 |
|---|---|
| 使用者登入後資料 | 可以 |
| 通知列表 | 可以 |
| 收藏狀態 | 可以 |
| dashboard 小卡 | 可以 |
| 需要 refetch 的資料 | 可以 |
| 初始 SEO 內容 | 不一定 |
| 文章詳情頁主內容 | 通常不用 |
如果資料跟 SEO 或初始內容強相關,我會優先 Server Component。
如果資料需要頻繁互動、重新整理、mutation、快取,就可以考慮 React Query。
---
Zustand 也是 Client State,所以使用 Zustand 的元件一定是 Client Component。
例如:
1'use client';
2
3import { useUiStore } from '@/stores/useUiStore';
4
5export function MobileMenuButton() {
6 const openMenu = useUiStore((state) => state.openMenu);
7
8 return <button onClick={openMenu}>Menu</button>;
9}Zustand 很適合放:
但我不會把 Server Component 為了使用 Zustand 直接改成 Client。
我會拆小:
1Layout.server
2 ├── Header.server
3 ├── MobileMenuButton.client
4 └── MobileDrawer.client---
| 問題 | 如果是 | 建議 |
|---|---|---|
| 需要 SEO 嗎? | 需要 | Server |
| 初始畫面就要資料嗎? | 需要 | Server |
| 需要使用者互動嗎? | 需要 | Client |
| 需要 `useState` 嗎? | 需要 | Client |
| 需要 Zustand 嗎? | 需要 | Client |
| 需要 React Query 嗎? | 需要 | Client |
| 需要讀 token / DB 嗎? | 需要 | Server |
| 只是排版或內容顯示嗎? | 是 | Server |
| 只是小按鈕互動嗎? | 是 | 拆成小 Client Component |
---
這通常只是讓錯誤消失,不代表架構正確。
比較好的做法是先問:
是整頁都需要 client,還是只有某個小元件需要?
---
例如:
這些不該放 Client。
---
這也很常見。
例如為了 theme、menu、auth 狀態,把整個 layout 改成 Client。
通常可以拆成 provider 或小元件,不一定要讓整個 layout client-side。
---
App Router 已經提供 Server Component 的資料取得模式。
如果是初始頁面內容,不一定要回到以前 SPA 的寫法。
---
我現在會用這個方向拆:
1Server Component
2 負責資料取得、SEO、權限前置檢查、靜態內容、版面骨架
3
4Client Component
5 負責互動、表單、瀏覽器 API、client state、mutation 操作更具體一點:
1Page.server
2 ├── Data Section.server
3 ├── Content Section.server
4 ├── SEO Content.server
5 └── Interactive Widget.client不要為了局部互動,把整個頁面變成 Client Component。
也不要為了追求 Server Component,把所有互動都寫得很彆扭。
重點是邊界要清楚。
---
Next.js App Router 的 Server Component / Client Component 切分,核心不是背規則。
而是先分清楚每個元件的責任。
| 責任 | 放哪裡 |
|---|---|
| 資料取得 | Server |
| SEO 內容 | Server |
| 靜態 UI | Server |
| 權限與敏感邏輯 | Server |
| 使用者互動 | Client |
| 表單狀態 | Client |
| 瀏覽器 API | Client |
| Zustand | Client |
| React Query | Client |
我自己的原則是:
頁面預設 Server,互動局部 Client。
這樣做可以保留 App Router 的優勢,也能避免整個專案慢慢退回傳統 SPA 寫法。
Server Component 不是要取代 Client Component。
Client Component 也不是不能用。
真正重要的是,不要讓一個小互動,拖著整個頁面一起變 Client。
切得清楚,資料流會比較穩,效能比較好,後面接手的人也比較容易看懂。