react-component-dev

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

React Component Development

React 组件开发

Patterns for building composable, accessible, well-structured React components.
构建可组合、可访问、结构清晰的React组件的模式指南。

Core Principles

核心原则

  1. Composition over configuration - Props enable customization, not enumerate options
  2. Forwarding refs - Components that render DOM elements forward refs
  3. Accessibility first - Keyboard, screen readers, reduced motion
  4. Predictable APIs - Consistent prop patterns across components
  1. 组合优于配置 - 通过Props实现自定义,而非枚举选项
  2. 转发Ref - 渲染DOM元素的组件需转发Ref
  3. 可访问性优先 - 支持键盘操作、屏幕阅读器、精简动画
  4. 可预测的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

必备属性

PropTypePurpose
className
string
Style composition
children
ReactNode
Content (when applicable)
...rest
native propsForward all valid HTML attributes
属性类型用途
className
string
样式组合
children
ReactNode
内容(适用时)
...rest
原生属性转发所有合法HTML属性

Variant Props

变体属性

tsx
// Good: Union of literal types
variant?: "default" | "destructive" | "outline"

// Bad: Boolean props that multiply
isPrimary?: boolean
isDestructive?: boolean
isOutline?: boolean
tsx
// 推荐:字面量类型联合
variant?: "default" | "destructive" | "outline"

// 不推荐:布尔属性数量膨胀
isPrimary?: boolean
isDestructive?: boolean
isOutline?: boolean

Render 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
    .focus()
    , measure, or attach refs
  • 组件渲染单个DOM元素
  • 组件包装其他forwardRef组件
  • 用户可能需要调用
    .focus()
    、测量尺寸或附加Ref

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
useState
for:
  • 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

速查参考

PatternWhen
forwardRef
Wrapping DOM elements
ComponentPropsWithoutRef<"tag">
Inheriting native props
cn()
Merging classNames
as const
Literal type inference
useImperativeHandle
Custom ref APIs (rare)
React.Children
Manipulating children (avoid if possible)
模式使用场景
forwardRef
包装DOM元素
ComponentPropsWithoutRef<"tag">
继承原生属性
cn()
合并className
as const
字面量类型推断
useImperativeHandle
自定义Ref API(罕见场景)
React.Children
操作子元素(尽可能避免)