Client Components

2026年9月6日

文件第一行写 'use client',这个模块以及它 import 的模块都会被当成客户端组件图的一部分。

"use client";

import { useEffect, useState } from "react";

export default function Counter() {
  const [n, setN] = useState(0);

  useEffect(() => {
    console.log(window.innerWidth);
  }, []);

  return <button onClick={() => setN((x) => x + 1)}>{n}</button>;
}
tsx

仍会在服务器预渲染

客户端组件不是「只在浏览器跑」。服务器会先画出一版静态 HTML(上面例子里能看到 0 和按钮),再在浏览器水合。所以:

  • 模块顶层、函数体里的 console.log 开发时可能打两次(一次服务器、一次客户端)
  • window / document 必须放进 useEffect 或事件回调,否则服务端预渲染会炸

嵌套方向

服务端 → 客户端可以。
客户端 → 服务端不行:客户端 bundler 会把子树都当成客户端模块,里面的 fs、密钥立刻出问题。

需要「客户端壳 + 服务端内容」时,让服务端当父亲,把客户端当孩子;或者把服务端结果通过 children 插进客户端:

// ServerParent.tsx(服务端)
import ClientTabs from "./ClientTabs";
import ServerList from "./ServerList";

export default function ServerParent() {
  return (
    <ClientTabs>
      <ServerList />
    </ClientTabs>
  );
}
tsx

ClientTabs 只负责切换 UI,不 import ServerList


server-only

fetchWebSocket 两边都能用,有人会写出「服务端客户端都能 import」的工具函数,一不小心把只该在服务器跑的逻辑带进 bundle。

pnpm add server-only
bash
import "server-only";

export function loadSecretConfig() {
  return { key: process.env.PRIVATE_KEY };
}
ts

客户端一旦 import 这个文件,构建会失败。反过来有 client-only,给只该在浏览器跑的模块用。


什么时候标 use client

  • 有 hooks、有事件、要读浏览器 API
  • 用了只能在客户端跑的库(部分图表、编辑器)

能留在服务端的就留下。'use client' 是边界,不是「这个文件比较重要」的标记。标得越高,进包的代码越多。

下一篇:Next 16 的 Cache Components,静态壳和动态洞怎么编在一起。


参考文档