返回博客

React 组件模式:别让设计模式反过来设计你

刚开始系统学 React 时,我把各种「模式」当成规范来背:组合优于继承、复合组件、Render Props、自定义 Hook……觉得写代码时能套上一个就算是在写「好代码」。

后来维护了一些积累了大量「模式」的旧代码库,才意识到:不少地方的复杂度正是这些模式带来的——不是因为模式本身有问题,而是它们被用在了不需要它们的地方。

模式解决的是什么问题

先说清楚每个模式存在的理由,再谈何时该用。

组合(Composition) 解决的是「父组件不应该知道子组件的内部结构」。Card + CardHeader + CardBody 比一个接受 titlecontent props 的 Card 更灵活——前者允许调用方自由组合,后者每次新增布局需求都要改 Card 的 API。但如果你的 Card 只有一种用法,那组合就是过早泛化,直接写死更清晰。

复合组件(Compound Components) 解决的是「一组有共享状态的 UI 单元需要协同工作」。Tabs、Accordion、Menu 是典型场景——Tab 需要知道当前激活的是哪一项,但这个状态不应该暴露给外部调用方去管理。通过 Context 内部共享,外部只看到 <Tabs><Tab /><TabPanel /></Tabs> 的干净结构。如果你的组件之间没有共享状态需求,强行做成复合组件只是在加层。

Render Props 解决的是「逻辑复用但渲染形式多变」。在自定义 Hook 普及之前,这是提取可复用逻辑的主流方案。现在多数场景 Hook 已经够用,Render Props 的使用场景收窄到「需要在 JSX 结构层面注入灵活性」的地方,比如虚拟列表的行渲染。

Context + Reducer 解决的是「跨层级状态共享 + 多来源更新」。它适合模块级的状态(一个功能区域的数据),不适合全局状态(整个应用的用户信息、主题、权限)——后者更适合专门的状态管理库。把所有 useState 都升级成 Context,换来的是不必要的重渲染和可读性下降。

性能优化的误区:过早 memo

useMemouseCallbackReact.memo 是性能工具,不是默认写法。

常见的误区是「所有函数都 useCallback、所有计算都 useMemo」。实际上 memo 本身有成本:每次渲染都要做依赖项对比。对于轻量的计算或很少触发重渲染的组件,memo 的开销可能比它节省的还多。

真正需要 memo 的场景是:

  • useCallback:函数作为 props 传给已经 React.memo 包裹的子组件,或者作为 useEffect 的依赖
  • useMemo:计算代价确实高(排序、过滤大数组、派生复杂数据结构)
  • React.memo:组件接受稳定引用的 props,且父组件频繁重渲染

性能优化的正确顺序是:先测量,再优化。不要凭感觉给每个函数都套上 useCallback。

心法:先有问题,再选模式

每引入一个模式前,先问一句:「我在解决什么具体问题?」——如果答不上来,这个模式就是过早抽象。好代码不是模式的堆砌,而是恰到好处地用最少的结构解决当前的问题。

可访问性和错误边界:这两个不要省

有两类「模式」值得默认开启,不做成本效益分析:

一是键盘导航和 ARIA。Dropdown、Modal、Combobox 这类交互组件,键盘支持不是加分项,是基线。role="combobox"aria-expanded、焦点管理(打开 Modal 时保存当前焦点,关闭时恢复)——这些写起来不麻烦,但不写就是欠债,迟早要还。

二是错误边界(Error Boundary)。React 的 unhandled error 会白屏整个应用。用 Error Boundary 包裹功能模块,最坏情况是这个模块挂掉,其余部分还能用。这是防御性编程最低成本的一步,却经常被跳过。

一句话心法

模式是解决特定复杂度的工具;没有那个复杂度,就不需要那个工具。