React 组件抽象的目标不是减少文件数量,而是让一个稳定的界面概念拥有清晰 API。先允许少量重复,等共同结构和变化点真实出现后再抽象,通常比一开始设计“万能组件”更容易维护。
本文合并旧版“抽象组件”与“类组件和函数组件”文章,重点放在现代函数组件和组合模式。
目录
- 函数组件是默认选择
- 什么时候应该抽象
- 用组合代替大量布尔 props
- 状态应该放在哪里
- 受控组件与非受控组件
- 抽象层级的常见错误
- Server 与 Client Component 边界
- 抽象前检查
- 参考资料
函数组件是默认选择
新代码通常使用函数组件和 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。
抽象前检查
- 这个概念是否已经真实重复?
- 哪些部分稳定,哪些部分经常变化?
- props 名称能否表达业务含义?
- 调用示例是否比复制 JSX 更容易读?
- 是否保留了 HTML 语义和无障碍能力?
- 删除抽象后是否反而更简单?
好的组件 API 通常很小,并允许通过组合扩展;坏的抽象则不断增加特殊开关。
参考资料
继续阅读
React Hooks 完整指南:状态、副作用与 Context
用一篇文章理清 useState、useEffect、useReducer、useContext 和自定义 Hook 的职责、选择方法与常见错误,包含可验证的 TypeScript 示例。
10 分钟React Hydration Failed:原因、定位与修复
系统排查服务器 HTML 与客户端首次渲染不一致的问题,覆盖时间、随机数、浏览器 API、无效 HTML、客户端存储、第三方扩展和 suppressHydrationWarning 的边界。
11 分钟Next.js 路由指南:App Router 常见模式
基于当前 Next.js App Router,梳理静态路由、动态路由、catch-all、route groups、parallel routes 和 route handlers 的使用场景,并说明什么时候还会遇到 Pages Router。
订阅 FreeMac
每周精选:免费 Mac 软件评测、可信来源更新、替代方案和少折腾指南。