react-component-dev
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseReact Component Development
React 组件开发
Patterns for building composable, accessible, well-structured React components.
构建可组合、可访问、结构清晰的React组件的模式指南。
Core Principles
核心原则
- Composition over configuration - Props enable customization, not enumerate options
- Forwarding refs - Components that render DOM elements forward refs
- Accessibility first - Keyboard, screen readers, reduced motion
- Predictable APIs - Consistent prop patterns across components
- 组合优于配置 - 通过Props实现自定义,而非枚举选项
- 转发Ref - 渲染DOM元素的组件需转发Ref
- 可访问性优先 - 支持键盘操作、屏幕阅读器、精简动画
- 可预测的API - 组件间保持一致的属性模式
Component Template
组件模板
tsx
import { forwardRef, type ComponentPropsWithoutRef } from "react"
import { cn } from "@/lib/utils"
type ButtonProps = ComponentPropsWithoutRef<"button"> & {
variant?: "default" | "outline" | "ghost"
size?: "sm" | "md" | "lg"
}
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant = "default", size = "md", ...props }, ref) => {
return (
<button
ref={ref}
className={cn(
"base-styles",
variantStyles[variant],
sizeStyles[size],
className
)}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, type ButtonProps }tsx
import { forwardRef, type ComponentPropsWithoutRef } from "react"
import { cn } from "@/lib/utils"
type ButtonProps = ComponentPropsWithoutRef<"button"> & {
variant?: "default" | "outline" | "ghost"
size?: "sm" | "md" | "lg"
}
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant = "default", size = "md", ...props }, ref) => {
return (
<button
ref={ref}
className={cn(
"base-styles",
variantStyles[variant],
sizeStyles[size],
className
)}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, type ButtonProps }Prop Design
属性设计
Always Include
必备属性
| Prop | Type | Purpose |
|---|---|---|
| | Style composition |
| | Content (when applicable) |
| native props | Forward all valid HTML attributes |
| 属性 | 类型 | 用途 |
|---|---|---|
| | 样式组合 |
| | 内容(适用时) |
| 原生属性 | 转发所有合法HTML属性 |
Variant Props
变体属性
tsx
// Good: Union of literal types
variant?: "default" | "destructive" | "outline"
// Bad: Boolean props that multiply
isPrimary?: boolean
isDestructive?: boolean
isOutline?: booleantsx
// 推荐:字面量类型联合
variant?: "default" | "destructive" | "outline"
// 不推荐:布尔属性数量膨胀
isPrimary?: boolean
isDestructive?: boolean
isOutline?: booleanRender Props / Slots
渲染属性/插槽
For complex customization:
tsx
type DialogProps = {
trigger?: ReactNode
title: ReactNode
description?: ReactNode
children: ReactNode
footer?: ReactNode
}针对复杂自定义场景:
tsx
type DialogProps = {
trigger?: ReactNode
title: ReactNode
description?: ReactNode
children: ReactNode
footer?: ReactNode
}forwardRef Patterns
forwardRef 模式
When to Use
使用场景
- Component renders a single DOM element
- Component wraps another forwardRef component
- Users might need to call , measure, or attach refs
.focus()
- 组件渲染单个DOM元素
- 组件包装其他forwardRef组件
- 用户可能需要调用、测量尺寸或附加Ref
.focus()
When to Skip
跳过场景
- Component renders multiple root elements
- Component is purely logic (hooks)
- Internal-only component never exposed to consumers
- 组件渲染多个根元素
- 组件纯逻辑实现(Hooks)
- 仅内部使用、不对外暴露的组件
Extracting Ref Type
提取Ref类型
tsx
// From DOM element
forwardRef<HTMLDivElement, Props>
// From another component
forwardRef<ComponentRef<typeof OtherComponent>, Props>tsx
// 从DOM元素提取
forwardRef<HTMLDivElement, Props>
// 从其他组件提取
forwardRef<ComponentRef<typeof OtherComponent>, Props>File Organization
文件组织
components/
└── button/
├── index.ts # Re-export: export { Button } from "./button"
├── button.tsx # Implementation
├── button.test.tsx # Tests
└── use-button-state.ts # Complex state logic (if needed)components/
└── button/
├── index.ts # 重导出:export { Button } from "./button"
├── button.tsx # 实现代码
├── button.test.tsx # 测试代码
└── use-button-state.ts # 复杂状态逻辑(按需添加)index.ts Pattern
index.ts 模式
tsx
export { Button, type ButtonProps } from "./button"Keep index.ts as pure re-exports. No logic.
tsx
export { Button, type ButtonProps } from "./button"保持index.ts仅作为重导出文件,不包含逻辑代码。
Accessibility Checklist
可访问性检查清单
Keyboard
键盘操作
- All interactive elements focusable
- Focus order matches visual order
- Focus visible (outline or ring)
- Escape closes modals/dropdowns
- Enter/Space activates buttons
- Arrow keys for menu navigation
- 所有交互元素可获取焦点
- 焦点顺序与视觉顺序一致
- 焦点可见(轮廓或高亮环)
- 按下Esc可关闭模态框/下拉菜单
- 按下Enter/Space可激活按钮
- 使用方向键导航菜单
ARIA
ARIA 规范
tsx
// Buttons with icons only
<button aria-label="Close dialog">
<XIcon aria-hidden="true" />
</button>
// Loading states
<button disabled aria-busy={isLoading}>
{isLoading ? <Spinner /> : "Submit"}
</button>
// Expandable content
<button aria-expanded={isOpen} aria-controls="panel-id">
Toggle
</button>tsx
// 仅含图标按钮
<button aria-label="Close dialog">
<XIcon aria-hidden="true" />
</button>
// 加载状态
<button disabled aria-busy={isLoading}>
{isLoading ? <Spinner /> : "Submit"}
</button>
// 可展开内容
<button aria-expanded={isOpen} aria-controls="panel-id">
Toggle
</button>Reduced Motion
精简动画
tsx
const prefersReducedMotion = useMediaQuery("(prefers-reduced-motion: reduce)")
// Or in CSS
@media (prefers-reduced-motion: reduce) {
* { animation-duration: 0.01ms !important; }
}tsx
const prefersReducedMotion = useMediaQuery("(prefers-reduced-motion: reduce)")
// 或在CSS中设置
@media (prefers-reduced-motion: reduce) {
* { animation-duration: 0.01ms !important; }
}State Management
状态管理
Local State
本地状态
Use for:
useState- UI state (open/closed, selected)
- Form inputs (controlled)
- Ephemeral data (hover, focus)
使用处理:
useState- UI状态(展开/收起、选中状态)
- 表单输入(受控组件)
- 临时数据(悬浮、焦点状态)
Derived State
派生状态
tsx
// Bad: useEffect to sync
const [fullName, setFullName] = useState("")
useEffect(() => {
setFullName(`${firstName} ${lastName}`)
}, [firstName, lastName])
// Good: useMemo
const fullName = useMemo(
() => `${firstName} ${lastName}`,
[firstName, lastName]
)
// Best: Just compute it (if cheap)
const fullName = `${firstName} ${lastName}`tsx
// 不推荐:用useEffect同步
const [fullName, setFullName] = useState("")
useEffect(() => {
setFullName(`${firstName} ${lastName}`)
}, [firstName, lastName])
// 推荐:使用useMemo
const fullName = useMemo(
() => `${firstName} ${lastName}`,
[firstName, lastName]
)
// 最佳方案:直接计算(若成本低)
const fullName = `${firstName} ${lastName}`Complex State
复杂状态
tsx
// useReducer for multi-field updates
const [state, dispatch] = useReducer(reducer, initialState)
// Or extract to custom hook
const dialog = useDialogState()tsx
// 多字段更新使用useReducer
const [state, dispatch] = useReducer(reducer, initialState)
// 或提取为自定义Hook
const dialog = useDialogState()Event Handlers
事件处理
Prop Naming
属性命名
tsx
// Internal handler
const handleClick = () => { ... }
// Prop callbacks: on[Event]
type Props = {
onClick?: () => void
onOpenChange?: (open: boolean) => void
onValueChange?: (value: string) => void
}tsx
// 内部处理函数
const handleClick = () => { ... }
// 属性回调:on[Event]
type Props = {
onClick?: () => void
onOpenChange?: (open: boolean) => void
onValueChange?: (value: string) => void
}Composing Handlers
组合处理函数
tsx
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ onClick, ...props }, ref) => {
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
// Internal logic
trackClick()
// Call user's handler
onClick?.(e)
}
return <button ref={ref} onClick={handleClick} {...props} />
}
)tsx
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ onClick, ...props }, ref) => {
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
// 内部逻辑
trackClick()
// 调用用户传入的处理函数
onClick?.(e)
}
return <button ref={ref} onClick={handleClick} {...props} />
}
)Testing Approach
测试方法
What to Test
测试内容
- User interactions (click, type, submit)
- Accessibility (keyboard nav, ARIA states)
- Conditional rendering
- Error states
- 用户交互(点击、输入、提交)
- 可访问性(键盘导航、ARIA状态)
- 条件渲染
- 错误状态
What NOT to Test
无需测试内容
- Implementation details (internal state values)
- Styling (unless critical to function)
- Third-party library internals
- 实现细节(内部状态值)
- 样式(除非对功能至关重要)
- 第三方库内部逻辑
Test Structure
测试结构
tsx
describe("Button", () => {
it("calls onClick when clicked", async () => {
const handleClick = vi.fn()
render(<Button onClick={handleClick}>Click me</Button>)
await userEvent.click(screen.getByRole("button"))
expect(handleClick).toHaveBeenCalledOnce()
})
it("is disabled when disabled prop is true", () => {
render(<Button disabled>Disabled</Button>)
expect(screen.getByRole("button")).toBeDisabled()
})
})tsx
describe("Button", () => {
it("点击时调用onClick", async () => {
const handleClick = vi.fn()
render(<Button onClick={handleClick}>Click me</Button>)
await userEvent.click(screen.getByRole("button"))
expect(handleClick).toHaveBeenCalledOnce()
})
it("传入disabled属性时处于禁用状态", () => {
render(<Button disabled>Disabled</Button>)
expect(screen.getByRole("button")).toBeDisabled()
})
})Anti-Patterns
反模式
Prop Drilling
属性透传
tsx
// Bad: Passing props through many layers
<Parent value={x} onChange={y}>
<Child value={x} onChange={y}>
<GrandChild value={x} onChange={y} />
// Better: Context for deep trees
<ValueContext.Provider value={{ x, onChange: y }}>
<Parent>
<Child>
<GrandChild /> {/* useContext inside */}tsx
// 不推荐:多层透传属性
<Parent value={x} onChange={y}>
<Child value={x} onChange={y}>
<GrandChild value={x} onChange={y} />
// 推荐:深层组件树使用Context
<ValueContext.Provider value={{ x, onChange: y }}>
<Parent>
<Child>
<GrandChild /> {/* 内部使用useContext */}Premature Abstraction
过早抽象
tsx
// Bad: Generic component nobody asked for
<FlexContainer direction="column" gap={4} align="center" justify="between">
// Good: Specific component for the use case
<CardHeader>tsx
// 不推荐:无人需要的通用组件
<FlexContainer direction="column" gap={4} align="center" justify="between">
// 推荐:针对具体场景的专用组件
<CardHeader>Boolean Prop Explosion
布尔属性膨胀
tsx
// Bad
<Button primary large disabled loading>
// Good
<Button variant="primary" size="lg" disabled isLoading>tsx
// 不推荐
<Button primary large disabled loading>
// 推荐
<Button variant="primary" size="lg" disabled isLoading>Quick Reference
速查参考
| Pattern | When |
|---|---|
| Wrapping DOM elements |
| Inheriting native props |
| Merging classNames |
| Literal type inference |
| Custom ref APIs (rare) |
| Manipulating children (avoid if possible) |
| 模式 | 使用场景 |
|---|---|
| 包装DOM元素 |
| 继承原生属性 |
| 合并className |
| 字面量类型推断 |
| 自定义Ref API(罕见场景) |
| 操作子元素(尽可能避免) |