⚠️ Alpha内测版本警告:此为早期内部构建版本,尚不完整且可能存在错误,欢迎大家提Issue反馈问题或建议
Skip to content

2.3.2 客户端边界与数据流

一句话破题

'use client' 不只是"让组件能用 useState"那么简单——它划定了一条边界,决定了哪些代码发送到浏览器,直接影响应用的性能和包体积。

边界的本质

关键规则

规则说明
默认是服务器组件不加 'use client' 就是 Server Component
边界向下传染客户端组件的子组件也是客户端组件
不能逆向导入客户端组件不能 import 服务器组件
Props 必须可序列化跨边界传递的 props 要能 JSON 序列化

正确划分边界

❌ 错误:整个页面都是客户端组件

typescript
// app/product/page.tsx
'use client'  // ❌ 太早声明!

export default function ProductPage() {
  const [quantity, setQuantity] = useState(1)
  
  return (
    <div>
      <ProductInfo />      {/* 本可以是服务器组件 */}
      <ProductImages />    {/* 本可以是服务器组件 */}
      <AddToCart quantity={quantity} setQuantity={setQuantity} />
    </div>
  )
}

✅ 正确:最小化客户端边界

typescript
// app/product/page.tsx
// 没有 'use client',是服务器组件
export default async function ProductPage({ params }) {
  const product = await getProduct(params.id)
  
  return (
    <div>
      <ProductInfo product={product} />    {/* 服务器组件 */}
      <ProductImages images={product.images} />  {/* 服务器组件 */}
      <AddToCart product={product} />      {/* 客户端组件 */}
    </div>
  )
}
typescript
// components/add-to-cart.tsx
'use client'

export function AddToCart({ product }) {
  const [quantity, setQuantity] = useState(1)
  
  return (
    <div>
      <QuantitySelector value={quantity} onChange={setQuantity} />
      <BuyButton product={product} quantity={quantity} />
    </div>
  )
}

Children 模式:突破边界限制

typescript
// ❌ 客户端组件不能直接导入服务器组件
'use client'
import { ServerComponent } from './server-component'  // 错误!

// ✅ 通过 children 传递
// layout.tsx (服务器组件)
export default function Layout({ children }) {
  return (
    <ClientProvider>  {/* 客户端组件 */}
      {children}       {/* 可以是服务器组件 */}
    </ClientProvider>
  )
}
typescript
// client-provider.tsx
'use client'

export function ClientProvider({ children }) {
  const [theme, setTheme] = useState('light')
  
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}  {/* 服务器组件作为 children 传入 */}
    </ThemeContext.Provider>
  )
}

Props 序列化问题

typescript
// ❌ 函数不能跨边界传递
<ClientComponent onSubmit={async (data) => { ... }} />  // 错误!

// ✅ 使用 Server Actions
<ClientComponent action={submitAction} />  // Server Action 可以!
typescript
// ❌ 复杂对象可能有问题
<ClientComponent data={new Map()} />  // Map 不能序列化

// ✅ 使用可序列化的数据
<ClientComponent data={Object.fromEntries(map)} />

性能对比

方案JS Bundle 大小首屏速度
全客户端大(包含所有组件)
最小边界小(只有交互组件)

觉知:常见错误

1. 不必要的 'use client'

typescript
// ❌ 纯展示组件不需要 'use client'
'use client'
export function ProductCard({ product }) {
  return <div>{product.name}</div>
}

// ✅ 移除 'use client'
export function ProductCard({ product }) {
  return <div>{product.name}</div>
}

2. 误解 "客户端组件也在服务器运行"

typescript
'use client'
// 这个组件会在服务器预渲染,然后在客户端 hydrate
export function Counter() {
  console.log('这行在服务器和客户端都会执行')
  const [count, setCount] = useState(0)
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}

本节小结

原则说明
默认服务器能用服务器组件就用服务器组件
最小边界'use client' 放在最需要的地方
Children 模式让服务器组件穿过客户端边界
Props 序列化跨边界数据必须可序列化