服务端与客户端组件

App Router 里,布局和页面默认都是 服务端组件(Server Components)。它们在服务器上运行,可以直接访问数据库、使用密钥,并把极少量 JavaScript 发到浏览器。需要交互时,你再加入 客户端组件(Client Components)

何时选哪一种?

选服务端组件

  • 靠近数据源获取数据(数据库、API)。
  • 使用 API Key、Token 等不能暴露给客户端的机密。
  • 减少发到浏览器的 JS,优化 FCP。
  • 渐进式流式输出。

选客户端组件

  • 需要 useStateuseEffect 等 hooks。
  • 需要浏览器 API(localStoragewindowNavigator.geolocation)。
  • 监听 onClickonChange 等事件。

在 Next.js 里,它们如何运作?

服务器侧

  • 服务端组件被渲染为一种特殊的数据格式:RSC Payload
  • 它与客户端组件的占位符一起,在服务端拼出首屏 HTML。

客户端首次加载

  1. HTML 立刻呈现一个非交互预览。
  2. RSC Payload 协调服务端/客户端组件树。
  3. JavaScript 加载并 水合 客户端组件,让页面变得可交互。

后续导航

  • RSC Payload 被预取并缓存,切换瞬间完成。
  • 客户端组件完全在浏览器里渲染。

写一个客户端组件

在文件顶部加 'use client' 指令:

'use client';

import { useState } from 'react';

export default function Counter() {
  const [n, setN] = useState(0);
  return (
    <div>
      <p>{n} 次点击</p>
      <button onClick={() => setN(n + 1)}>点我</button>
    </div>
  );
}

'use client' 声明了一道分界线:从该文件开始,它所有的 import 都属于客户端 bundle,无需每个文件都重复写指令。

减小 JS 体积的诀窍

'use client' 放在尽可能小的叶子组件上,而不是整块页面。例如:

// 布局:服务端组件
import Search from './search';   // 客户端组件
import Logo from './logo';       // 服务端组件

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <>
      <nav>
        <Logo />
        <Search />
      </nav>
      <main>{children}</main>
    </>
  );
}

整个布局依然是服务端组件,只有 Search 是客户端 JS。

服务端传递数据到客户端

直接用 props 即可(注意 props 必须可序列化):

// 服务端组件
import LikeButton from '@/app/ui/like-button';
import { getPost } from '@/lib/data';

export default async function Page({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  const post = await getPost(id);
  return <LikeButton likes={post.likes} />;
}
// 客户端组件
'use client';
export default function LikeButton({ likes }: { likes: number }) {
  // ...
}

在客户端组件里嵌套服务端组件

客户端组件不能直接 import 服务端组件,但可以通过 children 插槽 把它传进来:

// 客户端组件:Modal
'use client';
export default function Modal({ children }: { children: React.ReactNode }) {
  return <div className="modal">{children}</div>;
}
// 服务端组件:Page
import Modal from './ui/modal';
import Cart from './ui/cart'; // 服务端组件

export default function Page() {
  return (
    <Modal>
      <Cart />
    </Modal>
  );
}

这种「客户端外壳 + 服务端内容」的组合非常常见。

Context Provider

React Context 不被服务端组件支持。把 Provider 封装成客户端组件,在根布局里使用:

'use client';
import { createContext } from 'react';

export const ThemeContext = createContext({});

export default function ThemeProvider({ children }: { children: React.ReactNode }) {
  return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>;
}

防止「环境污染」

服务器代码可能意外 import 到客户端。用 server-only 包在构建期抛出错误:

import 'server-only';

export async function getData() {
  const res = await fetch('https://external-service.com/data', {
    headers: { authorization: process.env.API_KEY },
  });
  return res.json();
}

对应地也有 client-only 用于标记浏览器专属的模块。

伺服器與客戶端元件

App Router 中,佈局與頁面預設都是 伺服器元件(Server Components)。它們在伺服器上執行,可直接存取資料庫、使用密鑰,並把極少量 JavaScript 傳到瀏覽器。需要互動時,再加入 客戶端元件(Client Components)

何時選哪一種?

選伺服器元件

  • 在資料來源附近取得資料(資料庫、API)。
  • 使用 API Key、Token 等不能暴露給客戶端的機密。
  • 減少送到瀏覽器的 JS,優化 FCP。
  • 漸進式串流輸出。

選客戶端元件

  • 需要 useStateuseEffect 等 hooks。
  • 需要瀏覽器 API(localStoragewindowNavigator.geolocation)。
  • 監聽 onClickonChange 等事件。

在 Next.js 中如何運作?

伺服器端

  • 伺服器元件會被渲染成一種特殊的資料格式:RSC Payload
  • 它與客戶端元件的佔位符一起,在伺服器端拼出首屏 HTML。

客戶端首次載入

  1. HTML 立即呈現一個非互動預覽。
  2. RSC Payload 協調伺服器/客戶端元件樹。
  3. JavaScript 載入並 水合 客戶端元件,令頁面可互動。

後續導航

  • RSC Payload 會被預取並快取,切換瞬間完成。
  • 客戶端元件完全在瀏覽器中渲染。

撰寫一個客戶端元件

在檔案頂部加上 'use client' 指令:

'use client';

import { useState } from 'react';

export default function Counter() {
  const [n, setN] = useState(0);
  return (
    <div>
      <p>{n} 次點擊</p>
      <button onClick={() => setN(n + 1)}>點我</button>
    </div>
  );
}

'use client' 聲明了一條分界線:自此檔案起,所有的 import 皆屬於客戶端 bundle,不必每個檔案都重寫指令。

縮小 JS 體積的竅門

'use client' 放在盡可能小的葉子元件上,而不是整塊頁面:

// 佈局:伺服器元件
import Search from './search';   // 客戶端元件
import Logo from './logo';       // 伺服器元件

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <>
      <nav>
        <Logo />
        <Search />
      </nav>
      <main>{children}</main>
    </>
  );
}

整個佈局仍然是伺服器元件,只有 Search 是客戶端 JS。

從伺服器傳遞資料到客戶端

直接用 props 即可(props 必須可序列化):

// 伺服器元件
import LikeButton from '@/app/ui/like-button';
import { getPost } from '@/lib/data';

export default async function Page({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  const post = await getPost(id);
  return <LikeButton likes={post.likes} />;
}
// 客戶端元件
'use client';
export default function LikeButton({ likes }: { likes: number }) {
  // ...
}

在客戶端元件內嵌套伺服器元件

客戶端元件不能直接 import 伺服器元件,但可以透過 children 插槽 將它傳進來:

// 客戶端元件:Modal
'use client';
export default function Modal({ children }: { children: React.ReactNode }) {
  return <div className="modal">{children}</div>;
}
// 伺服器元件:Page
import Modal from './ui/modal';
import Cart from './ui/cart'; // 伺服器元件

export default function Page() {
  return (
    <Modal>
      <Cart />
    </Modal>
  );
}

這種「客戶端外殼 + 伺服器內容」的組合非常常見。

Context Provider

React Context 不被伺服器元件支援。把 Provider 封成客戶端元件,在根佈局中使用:

'use client';
import { createContext } from 'react';

export const ThemeContext = createContext({});

export default function ThemeProvider({ children }: { children: React.ReactNode }) {
  return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>;
}

防止「環境污染」

伺服器程式碼可能被意外 import 至客戶端。用 server-only 套件在建置期拋出錯誤:

import 'server-only';

export async function getData() {
  const res = await fetch('https://external-service.com/data', {
    headers: { authorization: process.env.API_KEY },
  });
  return res.json();
}

對應也有 client-only 用於標記瀏覽器專屬模組。

Server and Client Components

In the App Router, layouts and pages are Server Components by default. They run on the server, can reach databases and secrets directly, and ship very little JavaScript to the browser. When you need interactivity, you layer in Client Components.

When to use each

Server Components — use when you need to:

  • Fetch data close to its source (databases, APIs).
  • Use API keys, tokens, and other secrets.
  • Reduce client JS and improve First Contentful Paint.
  • Stream content progressively.

Client Components — use when you need:

  • Hooks like useState, useEffect.
  • Browser-only APIs (localStorage, window, Navigator.geolocation).
  • Event handlers like onClick, onChange.

How do they work in Next.js?

On the server:

  • Server Components render into a special data format called the RSC Payload.
  • Together with placeholders for Client Components, it's used to produce the initial HTML.

On the client (first load):

  1. HTML shows a fast, non-interactive preview.
  2. The RSC Payload reconciles the Server and Client trees.
  3. JavaScript hydrates Client Components to make the page interactive.

Subsequent navigations:

  • The RSC Payload is prefetched and cached for instant transitions.
  • Client Components render entirely in the browser.

Writing a Client Component

Add the 'use client' directive at the top of the file:

'use client';

import { useState } from 'react';

export default function Counter() {
  const [n, setN] = useState(0);
  return (
    <div>
      <p>{n} clicks</p>
      <button onClick={() => setN(n + 1)}>Click me</button>
    </div>
  );
}

'use client' declares a boundary: from this file onwards, all imports are part of the client bundle — you don't need the directive in every file.

Keep bundles lean

Push 'use client' as deep as possible. Mark only the interactive leaves, not whole pages:

// Layout: Server Component
import Search from './search';   // Client Component
import Logo from './logo';       // Server Component

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <>
      <nav>
        <Logo />
        <Search />
      </nav>
      <main>{children}</main>
    </>
  );
}

The layout remains a Server Component; only Search ships as client JS.

Passing data from Server to Client

Just use props — they must be serializable by React:

// Server Component
import LikeButton from '@/app/ui/like-button';
import { getPost } from '@/lib/data';

export default async function Page({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  const post = await getPost(id);
  return <LikeButton likes={post.likes} />;
}
// Client Component
'use client';
export default function LikeButton({ likes }: { likes: number }) {
  // ...
}

Interleaving: Server inside Client

Client Components can't import Server Components directly, but they can receive them as children:

// Client Component: Modal
'use client';
export default function Modal({ children }: { children: React.ReactNode }) {
  return <div className="modal">{children}</div>;
}
// Server Component: Page
import Modal from './ui/modal';
import Cart from './ui/cart'; // Server Component

export default function Page() {
  return (
    <Modal>
      <Cart />
    </Modal>
  );
}

This "client shell, server content" pattern is everywhere in practice.

Context providers

React Context isn't supported in Server Components. Wrap providers in a Client Component and mount them from the root layout:

'use client';
import { createContext } from 'react';

export const ThemeContext = createContext({});

export default function ThemeProvider({ children }: { children: React.ReactNode }) {
  return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>;
}

Preventing environment poisoning

Server code can accidentally be imported into the client. Use the server-only package to fail the build when that happens:

import 'server-only';

export async function getData() {
  const res = await fetch('https://external-service.com/data', {
    headers: { authorization: process.env.API_KEY },
  });
  return res.json();
}

client-only is the counterpart for browser-only modules.