React组件设计TypeScript前端架构

React 组件设计:组合、抽象与复用边界

从函数组件、props 和组合模式出发,说明什么时候值得抽象组件、如何避免过度封装,并给出受控组件、children、状态提升和自定义 Hook 的选择方法。

·更新于 ·阅读约 11 分钟·计算中...

React 组件抽象的目标不是减少文件数量,而是让一个稳定的界面概念拥有清晰 API。先允许少量重复,等共同结构和变化点真实出现后再抽象,通常比一开始设计“万能组件”更容易维护。

本文合并旧版“抽象组件”与“类组件和函数组件”文章,重点放在现代函数组件和组合模式。

目录

函数组件是默认选择

新代码通常使用函数组件和 Hooks:

type ButtonProps = {
  children: React.ReactNode
  variant?: "primary" | "secondary"
  onClick?: () => void
}

export function Button({ children, variant = "primary", onClick }: ButtonProps) {
  return (
    <button className={`button button-${variant}`} onClick={onClick}>
      {children}
    </button>
  )
}

类组件仍能运行,也可能出现在旧项目和 Error Boundary 中,但不要宣称函数组件天然“性能更高”。性能取决于渲染范围、状态位置、数据结构和实际测量;选择函数组件主要因为它与当前 React API 和生态模式一致。

什么时候应该抽象

满足以下两项以上再考虑:

  • 多处出现相同的可识别 UI 概念。
  • 结构相同,只有内容、状态或少量样式不同。
  • 交互规则需要统一修复或测试。
  • 组件能用清楚的业务名称描述,而不是 CommonWrapper2
  • 调用方不需要了解内部 DOM 细节。

只出现两次的三行 JSX 不一定需要组件;复制的复杂表单校验、无障碍行为和加载状态则应尽早集中。

用组合代替大量布尔 props

下面的 API 很快会失控:

<Card compact bordered showHeader showFooter imageLeft loading />

更可维护的方式是让调用方组合明确区域:

function Card({ children }: { children: React.ReactNode }) {
  return <article className="card">{children}</article>
}

Card.Header = function CardHeader({ children }: { children: React.ReactNode }) {
  return <header className="card-header">{children}</header>
}

Card.Body = function CardBody({ children }: { children: React.ReactNode }) {
  return <div className="card-body">{children}</div>
}

也可以通过普通 children、具名 props 或多个小组件完成,不必为了“高级模式”强行使用复合组件写法。

状态应该放在哪里

状态优先放在真正需要它的最近共同父组件:

  • 只有组件自己使用:组件内部 useState
  • 多个兄弟共享:提升到共同父组件。
  • 深层树共享主题或用户:Context。
  • 复杂状态转换:useReducer
  • 复用有状态逻辑:自定义 Hook。

不要把所有状态提前放进全局 store。状态离使用位置越远,理解更新来源和控制重渲染越困难。具体选择可参考 React Hooks 完整指南

受控组件与非受控组件

通用输入组件应明确由谁拥有状态。受控组件由调用方传值:

type SearchInputProps = {
  value: string
  onChange: (value: string) => void
}

function SearchInput({ value, onChange }: SearchInputProps) {
  return <input value={value} onChange={(event) => onChange(event.target.value)} />
}

非受控组件由 DOM 或组件内部保存当前值,适合简单表单或与原生 API 集成。不要让同一个组件在受控与非受控模式之间切换。

抽象层级的常见错误

  • 为单个页面创建大量只有一行包装的“公共组件”。
  • 一个组件接收十几个布尔参数控制完全不同的布局。
  • 把数据请求、业务权限、视觉样式和弹窗状态全塞进一个组件。
  • 为了避免 props 传递两层就引入全局 Context。
  • 通过 useEffect 同步两个本可由同一来源计算的 state。
  • 忽视语义 HTML、键盘操作和焦点管理,只复用视觉外观。

Server 与 Client Component 边界

Next.js App Router 中,数据展示组件优先留在服务端;需要 state、事件、Effect 或浏览器 API 的最小交互区域才标记 "use client"。不要因为一个按钮把整个页面树都变成客户端模块。详细边界见 Next.js Server 与 Client Components

抽象前检查

  1. 这个概念是否已经真实重复?
  2. 哪些部分稳定,哪些部分经常变化?
  3. props 名称能否表达业务含义?
  4. 调用示例是否比复制 JSX 更容易读?
  5. 是否保留了 HTML 语义和无障碍能力?
  6. 删除抽象后是否反而更简单?

好的组件 API 通常很小,并允许通过组合扩展;坏的抽象则不断增加特殊开关。

参考资料

订阅 FreeMac

每周精选:免费 Mac 软件评测、可信来源更新、替代方案和少折腾指南。