react-use-client-boundary

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

React "use client" Directive & Client Boundaries

React "use client" 指令与客户端边界

Understanding when to use (and when NOT to use) the "use client" directive in React Server Components architecture.
了解在React Server Components架构中何时使用(以及何时不使用)"use client"指令。

Core Concept: The Boundary

核心概念:边界

"use client"
marks a boundary between server and client components - not a label for individual components.
Critical Rule: Once inside a client boundary, ALL imported components are automatically client components. You should NOT add
"use client"
to child components that are already imported by a parent client component.
"use client"
标记了服务器组件与客户端组件之间的边界,而非单个组件的标签。
关键规则: 一旦进入客户端边界,所有导入的组件都会自动成为客户端组件。对于已被父级客户端组件导入的子组件,你不应该添加
"use client"

Mental Model: The Fence

思维模型:围栏

Think of
"use client"
as a fence or gate:
┌─────────────────────────────────────────────────────┐
│  SERVER TERRITORY                                   │
│  ┌─────────────┐                                    │
│  │ page.tsx    │  (Server Component - default)      │
│  │             │                                    │
│  │  <Header /> │───────────────────────┐            │
│  └─────────────┘                       │            │
│                                        ▼            │
│  ════════════════ "use client" FENCE ════════════   │
│                                        │            │
│  ┌─────────────────────────────────────┼──────────┐ │
│  │ CLIENT TERRITORY                    ▼          │ │
│  │  ┌─────────────┐    ┌─────────────┐            │ │
│  │  │ Header.tsx  │───▶│ NavMenu.tsx │            │ │
│  │  │"use client" │    │ (no directive│            │ │
│  │  │             │    │  needed!)    │            │ │
│  │  └─────────────┘    └─────────────┘            │ │
│  │                                                 │ │
│  │  You're already inside - no more fences needed │ │
│  └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
可以将
"use client"
想象成一道围栏大门
┌─────────────────────────────────────────────────────┐
│  SERVER TERRITORY                                   │
│  ┌─────────────┐                                    │
│  │ page.tsx    │  (Server Component - default)      │
│  │             │                                    │
│  │  <Header /> │───────────────────────┐            │
│  └─────────────┘                       │            │
│                                        ▼            │
│  ════════════════ "use client" FENCE ════════════   │
│                                        │            │
│  ┌─────────────────────────────────────┼──────────┐ │
│  │ CLIENT TERRITORY                    ▼          │ │
│  │  ┌─────────────┐    ┌─────────────┐            │ │
│  │  │ Header.tsx  │───▶│ NavMenu.tsx │            │ │
│  │  │"use client" │    │ (no directive│            │ │
│  │  │             │    │  needed!)    │            │ │
│  │  └─────────────┘    └─────────────┘            │ │
│  │                                                 │ │
│  │  You're already inside - no more fences needed │ │
│  └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘

When to Use "use client"

何时使用"use client"

Add the directive when ALL of these are true:
  1. The component is imported by a Server Component (directly or as a page entry)
  2. AND the component needs client-side features:
    • React hooks (
      useState
      ,
      useEffect
      ,
      useContext
      , etc.)
    • Event handlers (
      onClick
      ,
      onChange
      ,
      onSubmit
      , etc.)
    • Browser APIs (
      window
      ,
      document
      ,
      localStorage
      , etc.)
    • Third-party libraries that use any of the above
当以下所有条件都满足时添加该指令:
  1. 组件被Server Component导入(直接导入或作为页面入口)
  2. 并且组件需要客户端特性:
    • React钩子(
      useState
      useEffect
      useContext
      等)
    • 事件处理器(
      onClick
      onChange
      onSubmit
      等)
    • 浏览器API(
      window
      document
      localStorage
      等)
    • 使用了上述任意特性的第三方库

When NOT to Use "use client"

何时不使用"use client"

  1. Already inside a client boundary - parent component has
    "use client"
  2. Component is pure presentation - just renders props, no interactivity
  3. "Just to be safe" - this creates confusion and unnecessary boundaries
  4. Every component that uses props - props work fine in server components
  1. 已处于客户端边界内 - 父组件已添加
    "use client"
  2. 组件为纯展示型 - 仅渲染props,无交互性
  3. "只是为了保险起见" - 这会造成混淆并产生不必要的边界
  4. 所有使用props的组件 - props在服务器组件中可以正常工作

Common Mistake: Redundant Directives

常见错误:冗余指令

tsx
// ❌ WRONG: Unnecessary "use client" in child

// components/form.tsx
"use client"
import { Input } from "./input"
import { Button } from "./button"

export function Form() {
  const [value, setValue] = useState("")
  return (
    <form>
      <Input value={value} onChange={setValue} />
      <Button type="submit">Send</Button>
    </form>
  )
}

// components/input.tsx
"use client"  // ❌ WRONG - already a client component!
export function Input({ value, onChange }) {
  return <input value={value} onChange={e => onChange(e.target.value)} />
}

// components/button.tsx
"use client"  // ❌ WRONG - already a client component!
export function Button({ children, type }) {
  return <button type={type}>{children}</button>
}
tsx
// ❌ 错误:子组件中不必要的"use client"

// components/form.tsx
"use client"
import { Input } from "./input"
import { Button } from "./button"

export function Form() {
  const [value, setValue] = useState("")
  return (
    <form>
      <Input value={value} onChange={setValue} />
      <Button type="submit">Send</Button>
    </form>
  )
}

// components/input.tsx
"use client"  // ❌ 错误 - 已属于客户端组件!
export function Input({ value, onChange }) {
  return <input value={value} onChange={e => onChange(e.target.value)} />
}

// components/button.tsx
"use client"  // ❌ 错误 - 已属于客户端组件!
export function Button({ children, type }) {
  return <button type={type}>{children}</button>
}

Correct Approach: Single Boundary

正确做法:单一边界

tsx
// ✅ CORRECT: Only the entry point has "use client"

// components/form.tsx
"use client"
import { Input } from "./input"
import { Button } from "./button"

export function Form() {
  const [value, setValue] = useState("")
  return (
    <form>
      <Input value={value} onChange={setValue} />
      <Button type="submit">Send</Button>
    </form>
  )
}

// components/input.tsx
// ✅ No directive - imported by client component
export function Input({ value, onChange }) {
  return <input value={value} onChange={e => onChange(e.target.value)} />
}

// components/button.tsx
// ✅ No directive - imported by client component
export function Button({ children, type }) {
  return <button type={type}>{children}</button>
}
tsx
// ✅ 正确:仅在入口点添加"use client"

// components/form.tsx
"use client"
import { Input } from "./input"
import { Button } from "./button"

export function Form() {
  const [value, setValue] = useState("")
  return (
    <form>
      <Input value={value} onChange={setValue} />
      <Button type="submit">Send</Button>
    </form>
  )
}

// components/input.tsx
// ✅ 无需指令 - 被客户端组件导入
export function Input({ value, onChange }) {
  return <input value={value} onChange={e => onChange(e.target.value)} />
}

// components/button.tsx
// ✅ 无需指令 - 被客户端组件导入
export function Button({ children, type }) {
  return <button type={type}>{children}</button>
}

Decision Flowchart

决策流程图

Is this component imported by a Server Component?
├─ NO ──▶ Is its parent/importer a Client Component?
│         │
│         ├─ YES ──▶ ❌ Don't add "use client" (already in boundary)
│         │
│         └─ NO ───▶ Check the import chain upward
└─ YES ─▶ Does this component need client features?
          ├─ NO ──▶ ❌ Don't add "use client" (keep it server)
          └─ YES ─▶ ✅ Add "use client" (create boundary here)
该组件是否被Server Component导入?
├─ 否 ──▶ 它的父组件/导入者是Client Component吗?
│         │
│         ├─ 是 ──▶ ❌ 不要添加"use client" (已处于边界内)
│         │
│         └─ 否 ───▶ 向上检查导入链
└─ 是 ─▶ 该组件是否需要客户端特性?
          ├─ 否 ──▶ ❌ 不要添加"use client" (保持为服务器组件)
          └─ 是 ─▶ ✅ 添加"use client" (在此处创建边界)

Real-World Example: Page with Interactive Section

实际示例:包含交互区域的页面

tsx
// app/products/page.tsx (Server Component - no directive)
import { ProductList } from "@/components/product-list"
import { SearchFilters } from "@/components/search-filters"
import { getProducts } from "@/lib/api"

export default async function ProductsPage() {
  const products = await getProducts()  // Server-side data fetching
  
  return (
    <main>
      <h1>Products</h1>
      <SearchFilters />           {/* Client boundary starts here */}
      <ProductList data={products} />  {/* Server component */}
    </main>
  )
}

// components/search-filters.tsx
"use client"  // ✅ Boundary: imported by server, needs state
import { FilterDropdown } from "./filter-dropdown"
import { PriceSlider } from "./price-slider"

export function SearchFilters() {
  const [filters, setFilters] = useState({})
  
  return (
    <div>
      <FilterDropdown onSelect={...} />  {/* No directive needed */}
      <PriceSlider onChange={...} />      {/* No directive needed */}
    </div>
  )
}

// components/filter-dropdown.tsx
// ✅ No "use client" - already inside client boundary
export function FilterDropdown({ onSelect }) {
  return <select onChange={e => onSelect(e.target.value)}>...</select>
}

// components/price-slider.tsx
// ✅ No "use client" - already inside client boundary
export function PriceSlider({ onChange }) {
  return <input type="range" onChange={e => onChange(e.target.value)} />
}
tsx
// app/products/page.tsx (Server Component - 无指令)
import { ProductList } from "@/components/product-list"
import { SearchFilters } from "@/components/search-filters"
import { getProducts } from "@/lib/api"

export default async function ProductsPage() {
  const products = await getProducts()  // 服务器端数据获取
  
  return (
    <main>
      <h1>Products</h1>
      <SearchFilters />           {/* 客户端边界从此处开始 */}
      <ProductList data={products} />  {/* 服务器组件 */}
    </main>
  )
}

// components/search-filters.tsx
"use client"  // ✅ 边界:被服务器组件导入,需要状态管理
import { FilterDropdown } from "./filter-dropdown"
import { PriceSlider } from "./price-slider"

export function SearchFilters() {
  const [filters, setFilters] = useState({})
  
  return (
    <div>
      <FilterDropdown onSelect={...} />  {/* 无需指令 */}
      <PriceSlider onChange={...} />      {/* 无需指令 */}
    </div>
  )
}

// components/filter-dropdown.tsx
// ✅ 无需"use client" - 已处于客户端边界内
export function FilterDropdown({ onSelect }) {
  return <select onChange={e => onSelect(e.target.value)}>...</select>
}

// components/price-slider.tsx
// ✅ 无需"use client" - 已处于客户端边界内
export function PriceSlider({ onChange }) {
  return <input type="range" onChange={e => onChange(e.target.value)} />
}

Edge Case: Shared Components

边缘情况:共享组件

When a component is used by BOTH server and client components:
tsx
// components/card.tsx
// No directive - works in both contexts if it's pure presentation
export function Card({ title, children }) {
  return (
    <div className="card">
      <h2>{title}</h2>
      {children}
    </div>
  )
}

// app/page.tsx (Server Component)
import { Card } from "@/components/card"
// Card renders as server component here

// components/modal.tsx
"use client"
import { Card } from "@/components/card"
// Card renders as client component here (inside boundary)
当组件同时被服务器组件和客户端组件使用时:
tsx
// components/card.tsx
// 无指令 - 如果是纯展示型组件,可在两种上下文环境中工作
export function Card({ title, children }) {
  return (
    <div className="card">
      <h2>{title}</h2>
      {children}
    </div>
  )
}

// app/page.tsx (Server Component)
import { Card } from "@/components/card"
// 此处Card以服务器组件形式渲染

// components/modal.tsx
"use client"
import { Card } from "@/components/card"
// 此处Card以客户端组件形式渲染 (处于边界内)

Troubleshooting Common Errors

常见错误排查

Error: "useState only works in Client Components"

错误:"useState only works in Client Components"

Cause: Using hooks in a component without
"use client"
that's imported by a server component.
Fix: Add
"use client"
to the component using the hook, OR move the hook usage to a parent client component.
原因: 在未添加
"use client"
且被服务器组件导入的组件中使用了钩子。
修复: 为使用钩子的组件添加
"use client"
,或将钩子的使用移至父级客户端组件中。

Error: "Event handlers cannot be passed to Client Components from Server Components"

错误:"Event handlers cannot be passed to Client Components from Server Components"

Cause: Trying to pass a function from server to client component.
Fix: Move the event handler logic to the client component, or restructure the boundary.
原因: 尝试从服务器组件向客户端组件传递函数。
修复: 将事件处理器逻辑移至客户端组件,或重构边界结构。

Error: "async/await is not yet supported in Client Components"

错误:"async/await is not yet supported in Client Components"

Cause: Using async component syntax inside a client boundary.
Fix: Keep data fetching in server components, pass data as props to client components.
原因: 在客户端边界内使用了异步组件语法。
修复: 保持数据获取在服务器组件中进行,将数据作为props传递给客户端组件。

Best Practices Summary

最佳实践总结

DoDon't
Place
"use client"
at the highest necessary point
Sprinkle
"use client"
on every component
Keep the client boundary as small as possibleMake entire pages client components
Let child components inherit client contextAdd redundant
"use client"
to children
Use server components for data fetchingFetch data in client components when avoidable
应该做不应该做
"use client"
放在最高必要层级
在每个组件上都添加
"use client"
尽可能缩小客户端边界范围将整个页面设为客户端组件
让子组件继承客户端上下文为子组件添加冗余的
"use client"
使用服务器组件进行数据获取尽可能避免在客户端组件中获取数据

References

参考资料