随着 Next.js 13/14 全面推广 App Router 架构,“服务器组件”与“客户端组件”的边界划分成为开发者必须面对的课题。在实际开发中,不少团队在封装 next/link 的 Link 组件时遇到困惑:为什么直接使用 Link 完全正常,但自己写了一个包装组件却频频报错?这背后正是 'use client' 指令在发挥作用。本文将从原理、实例到最佳实践,为你厘清两者区别。
一、组件模型基础:服务器组件默认,客户端组件需声明
Next.js App Router 默认将页面内的所有组件视为“服务器组件”(Server Components)。服务器组件只在服务端执行,不包含交互逻辑(如 onClick、useState、useEffect),不生成客户端 JavaScript。若要使用浏览器端的交互能力,则必须在文件顶部添加 'use client' 指令,将该文件标记为“客户端组件”(Client Components)。这是 Next.js 优化性能、减少客户端代码体积的核心机制。
二、Link 组件的特殊身份:内建客户端组件但受“豁免”
next/link 导出的 Link 组件本质上是一个客户端组件——它需要监听点击事件、管理路由预取。然而,Next.js 对 Link 做了特殊处理:在服务器组件中直接使用 <Link> 是安全的,不会引发错误。因为框架会自动识别 Link 并为其生成必要的客户端边界,而无需开发者手动添加 'use client'。这意味着你可以这样写:
// 这是一个服务器组件文件,没有 'use client'
import Link from 'next/link';
export default function Nav() {
return <Link href="/about">关于我们</Link>;
}
无需任何额外声明。
三、Link 包装的陷阱:为什么你的自定义组件会崩溃?
当你试图将 Link 封装进一个自定义组件时,情况变得微妙。例如定义一个 CustomLink:
// CustomLink.tsx — 没有 'use client'
import Link from 'next/link';
export default function CustomLink({ href, children }) {
// 这里没有使用任何客户端 hooks
return <Link href={href}>{children}</Link>;
}
这个包装本身没有引入客户端逻辑,所以在服务器组件中使用 CustomLink 同样可以正常运行。问题出在包装组件内部使用了客户端特性。比如希望通过 useState 记录点击次数:
// CustomLink.tsx — 错误示例
import Link from 'next/link';
import { useState } from 'react'; // ❌ 未声明 'use client' 却使用客户端 hook
export default function CustomLink({ href, children }) {
const [count, setCount] = useState(0);
return (
<Link href={href} onClick={() => setCount(c => c+1)}>
{children} (点击{count}次)
</Link>
);
}
此时,Next.js 编译会抛出错误:“You're importing a component that needs useState. It only works in a Client Component but none of its parents are marked with 'use client'.” 原因是这个包装组件没有声明 'use client',却被当作服务器组件执行,而服务器环境中不存在 useState。
更隐蔽的场景:包装组件本身没有 hooks,但它的子组件(比如一个图标组件)使用了客户端 API。只要链条中任何一个节点需要客户端能力,且缺少 'use client' 声明,就会出错。
四、最佳实践:何时该用 'use client'?
-
纯展示性包装:如果自定义 Link 组件只是透传
href、className等静态属性,且不引入任何 React 状态、事件监听(除了 Link 自身的点击导航)或浏览器 API,可以不加'use client'。但为了团队代码可读性,建议在组件顶部加注释说明。 -
交互性包装:一旦自定义组件内使用了
useState、useEffect、onClick自定义回调、useRouter(客户端版本)或任何第三方客户端库,必须添加'use client'。 -
保守策略:官方文档推荐,当不确定时,直接添加
'use client'。虽然会将该组件及其所有子组件强制转为客户端组件(可能导致不必要的客户端代码),但能避免运行时错误。后续可通过 React 编译器、懒加载等优化。
五、总结:边界意识是 Next.js 现代开发的关键
Link 组件本身是内建的客户端组件,但 Next.js 为其提供了“免声明”特权。而 Link 包装则打破了这个特权:自定义组件不再是框架内建组件,需要开发者自行判断客户端边界。'use client' 就是主动声明“我这个文件里的组件需要在浏览器运行”的标识。
两者并非非此即彼的对立关系,而是不同层级的工具:Link 提供了开箱即用的客户端导航能力;'use client' 则是开发者手动划定客户端领域的开关。只有深刻理解服务器组件与客户端组件的边界,才能在 Next.js 中写出既高效又健壮的代码。
正如 Next.js 核心团队在多次演讲中强调的:“‘use client’ 不是用来标记所有组件的,而是用来标记那些真正需要浏览器能力的组件。” 对于 Link 的包装,问问自己:我需要让它拥有超出导航本身的行为吗?答案决定了你是否需要写下那行关键的指令。