react-use-client-boundary
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseReact "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"Critical Rule: Once inside a client boundary, ALL imported components are automatically client components. You should NOT add to child components that are already imported by a parent client component.
"use client""use client"关键规则: 一旦进入客户端边界,所有导入的组件都会自动成为客户端组件。对于已被父级客户端组件导入的子组件,你不应该添加。
"use client"Mental Model: The Fence
思维模型:围栏
Think of as a fence or gate:
"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 │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘可以将想象成一道围栏或大门:
"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:
- The component is imported by a Server Component (directly or as a page entry)
- AND the component needs client-side features:
- React hooks (,
useState,useEffect, etc.)useContext - Event handlers (,
onClick,onChange, etc.)onSubmit - Browser APIs (,
window,document, etc.)localStorage - Third-party libraries that use any of the above
- React hooks (
当以下所有条件都满足时添加该指令:
- 组件被Server Component导入(直接导入或作为页面入口)
- 并且组件需要客户端特性:
- React钩子(、
useState、useEffect等)useContext - 事件处理器(、
onClick、onChange等)onSubmit - 浏览器API(、
window、document等)localStorage - 使用了上述任意特性的第三方库
- React钩子(
When NOT to Use "use client"
何时不使用"use client"
- Already inside a client boundary - parent component has
"use client" - Component is pure presentation - just renders props, no interactivity
- "Just to be safe" - this creates confusion and unnecessary boundaries
- Every component that uses props - props work fine in server components
- 已处于客户端边界内 - 父组件已添加
"use client" - 组件为纯展示型 - 仅渲染props,无交互性
- "只是为了保险起见" - 这会造成混淆并产生不必要的边界
- 所有使用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 that's imported by a server component.
"use client"Fix: Add to the component using the hook, OR move the hook usage to a parent client component.
"use client"原因: 在未添加且被服务器组件导入的组件中使用了钩子。
"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
最佳实践总结
| Do | Don't |
|---|---|
Place | Sprinkle |
| Keep the client boundary as small as possible | Make entire pages client components |
| Let child components inherit client context | Add redundant |
| Use server components for data fetching | Fetch data in client components when avoidable |
| 应该做 | 不应该做 |
|---|---|
将 | 在每个组件上都添加 |
| 尽可能缩小客户端边界范围 | 将整个页面设为客户端组件 |
| 让子组件继承客户端上下文 | 为子组件添加冗余的 |
| 使用服务器组件进行数据获取 | 尽可能避免在客户端组件中获取数据 |