React 组件设计模式:从 HOC 到 Render Props 再到 Hooks
组件逻辑复用是 React 开发中的永恒话题。本文从实战角度对比三种主流方案,附真实踩坑经验。
1. 为什么需要组件设计模式
写 React 代码时,你一定遇到过这种场景:
┌─────────────────────────────────────┐
│ Header │ 需要相同的逻辑 │
├─────────────────────────────────────┤
│ Sidebar │ • 用户权限判断 │
│ │ • 数据订阅 │
│ Content │ • 日志记录 │
│ │ • 主题切换 │
└─────────────────────────────────────┘
同样的逻辑要在多个组件里重复?传统的组件复用方式有两种:
- Mixin(已废弃)—— React 16 之前的方式,问题太多已被移除
- 组件组合—— 今天要讲的三种模式,都是组合思想的体现
三种模式的演进:
HOC (2015) → Render Props (2017) → Hooks (2019)
下面从实操角度对比这三种方案。
2. HOC:曾经的主流方案
2.1 什么是 HOC
高阶组件(Higher-Order Component) 就是一个函数,接受一个组件,返回一个新组件。
const EnhancedComponent = higherOrderComponent(WrappedComponent);
组件把 props 转成 UI,HOC 把组件转成另一个组件。
2.2 解决什么问题
HOC 主要解决**横切关注点(Cross-Cutting Concerns)**问题。比如数据订阅:
// 不使用 HOC - 每个组件都要写一遍
class UserList extends React.Component {
state = { users: [] };
componentDidMount() {
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
DataSource.removeChangeListener(this.handleChange);
}
handleChange = () => {
this.setState({ users: DataSource.getUsers() });
};
render() {
return <ul>{/* render users */}</ul>;
}
}
class UserProfile extends React.Component {
// 同样的逻辑又要写一遍...
state = { user: null };
componentDidMount() {
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
DataSource.removeChangeListener(this.handleChange);
}
handleChange = () => {
this.setState({ user: DataSource.getUserById(this.props.userId) });
};
render() {
return <div>{/* render user */}</div>;
}
}
用 HOC 封装这个逻辑:
// withData HOC
function withData(getData) {
return function(WrappedComponent) {
return class extends React.Component {
state = { data: null };
componentDidMount() {
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
DataSource.removeChangeListener(this.handleChange);
}
handleChange = () => {
this.setState({ data: getData(DataSource, this.props) });
};
render() {
return <WrappedComponent data={this.state.data} {...this.props} />;
}
};
};
}
// 使用
const UserListWithData = withData(() => DataSource.getUsers())(UserList);
const UserProfileWithData = withData((ds, props) => ds.getUserById(props.userId))(UserProfile);
优点:逻辑复用、关注点分离(容器组件管数据,UI组件管渲染)
2.3 ⚠️ 踩坑经验
坑1:静态方法丢失
WrappedComponent.staticMethod = function() { return 'hello'; };
const Enhanced = withData()(WrappedComponent);
Enhanced.staticMethod; // ❌ undefined
解决:用 hoist-non-react-statics
import hoistNonReactStatic from 'hoist-non-react-statics';
function withData(getData) {
return function(WrappedComponent) {
class WithData extends React.Component { /* ... */ }
hoistNonReactStatic(WithData, WrappedComponent);
return WithData;
};
}
坑2:Refs 不传递
HOC 返回的是新组件,ref 指向的是外层容器,不是内部 WrappedComponent。
// ❌ ref 指向 WithData,而非 UserList
<UserListWithData ref={this.userListRef} />
// ✅ 用 forwardRef(React 16.3+)
const UserListWithData = React.forwardRef(
withData()(UserList)
);
坑3:不要在 render 内使用 HOC
// ❌ 每次 render 都创建新组件,整个子树卸载重挂
render() {
return <withData(UserList) />;
}
// ✅ 组件外使用
const UserListWithData = withData()(UserList);
render() {
return <UserListWithData />;
}
教训:这个坑我踩过,导致表单输入框每次都丢失焦点。
坑4:Props 属性覆盖
// HOC 注入了 data,但用户也传了 data prop
<UserListWithData data={customData} />
// HOC 传的 data 被覆盖了
原则:HOC 应该传递"无关"的 props 给 WrappedComponent。
3. Render Props:更灵活的共享方式
3.1 核心概念
Render Props 是带有一个函数 prop 的组件,组件调用这个函数来决定渲染什么。
<DataProvider render={data => (
<h1>Hello {data.target}</h1>
)}/>
其实不一定要叫 render,任何函数 prop 都可以:
// children 作为 render prop 更常见
<Mouse>
{mouse => (
<div>鼠标位置: {mouse.x}, {mouse.y}</div>
)}
</Mouse>
3.2 实战示例
用 Render Props 实现鼠标追踪:
class Mouse extends React.Component {
state = { x: 0, y: 0 };
handleMouseMove = (e) => {
this.setState({ x: e.clientX, y: e.clientY });
};
render() {
return (
<div onMouseMove={this.handleMouseMove}>
{this.props.render(this.state)}
</div>
);
}
}
// 使用 - 渲染猫
<Mouse render={mouse => <Cat mouse={mouse} />} />
// 同一个 Mouse 组件,渲染不同的东西
<Mouse render={mouse => (
<p>坐标:{mouse.x}, {mouse.y}</p>
)} />
3.3 对比 HOC
| 方面 | HOC | Render Props |
|---|---|---|
| 嵌套深度 | 多个 HOC 组合变深 | 嵌套在组件内部,可读性更好 |
| Props | 容易冲突(被覆盖) | 无冲突,render 函数接收明确的数据 |
| 实现 | 需要函数+类组件 | 普通组件即可 |
| 组合 | compose 组合 | 直接嵌套 |
3.4 ⚠️ 踩坑:与 PureComponent 冲突
这是最容易忽略的性能问题:
// ❌ 每次 render 都创建新函数,PureComponent 白费
class MouseTracker extends React.Component {
render() {
return (
<Mouse render={mouse => <Cat mouse={mouse} />} />
);
}
}
// ✅ 解决:定义为实例方法
class MouseTracker extends React.Component {
renderTheCat = (mouse) => {
return <Cat mouse={mouse} />;
};
render() {
return <Mouse render={this.renderTheCat} />;
}
}
原理:React.PureComponent 用浅比较决定要不要重新渲染。函数是对象,每次 render 创建新引用,浅比较永远 false,所以每次都重新渲染,PureComponent 的优化完全失效。
4. Hooks:终结嵌套地狱
4.1 自定义 Hook 的诞生
React 16.8 引入 Hooks,其中自定义 Hook 直接替代了 HOC 和 Render Props 的工作。
核心思想:不是用组件来复用逻辑,而是用函数来复用带状态的逻辑。
// useMouse Hook
function useMouse() {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMouseMove = (e) => {
setPosition({ x: e.clientX, y: e.clientY });
};
window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, []);
return position;
}
// 使用 - 太清爽了
function Cat() {
const mouse = useMouse();
return <div>猫在 {mouse.x}, {mouse.y}</div>;
}
4.2 用 Hook 替代 HOC/Render Props
替代 HOC:数据订阅
// HOC 写法
const UserListWithData = withData(() => DataSource.getUsers())(UserList);
// Hook 写法
function useData(getData) {
const [data, setData] = useState(null);
useEffect(() => {
const handler = () => setData(getData(DataSource));
DataSource.addChangeListener(handler);
return () => DataSource.removeChangeListener(handler);
}, [getData]);
return data;
}
// 使用 - getData 用 useCallback 缓存,避免 effect 频繁执行
function UserList() {
const getUsers = useCallback(() => DataSource.getUsers(), []);
const users = useData(getUsers);
return <ul>{/* render users */}</ul>;
}
替代 Render Props:鼠标追踪
// Render Props 写法
<Mouse render={mouse => <Cat mouse={mouse} />} />
// Hook 写法
<Cat mouse={useMouse()} />
4.3 状态隔离原理
关键点:每次调用 Hook,都会得到完全独立的 state。
function Counter() {
const [count, setCount] = useState(0); // 独立的 count
// ...
}
function App() {
return (
<>
<Counter /> {/* 第一个 count 实例 */}
<Counter /> {/* 第二个 count 实例,互不影响 */}
</>
);
}
这就是为什么 Hooks "自动"解决了 HOC 的 props 冲突和嵌套问题——根本没有嵌套。
4.4 Hooks 的优势
| 方面 | HOC/Render Props | Hooks |
|---|---|---|
| 嵌套 | 层层包裹 | 无嵌套 |
| Props | 冲突风险 | 无冲突 |
| 静态方法 | 需特殊处理 | 完全保留 |
| TypeScript | 一般 | 优秀(类型推断) |
| Tree Shaking | 部分支持 | 完全支持 |
| 学习成本 | 需理解两个概念 | 统一为 Hook |
5. 三种模式实战对比
场景:实现一个"带日志功能的组件"
HOC 版本
function withLogger(WrappedComponent) {
return class extends React.Component {
componentDidMount() {
console.log(`${WrappedComponent.name} mounted`);
}
componentDidUpdate(prevProps) {
console.log(`${WrappedComponent.name} updated:`, prevProps, '->', this.props);
}
componentWillUnmount() {
console.log(`${WrappedComponent.name} will unmount`);
}
render() {
return <WrappedComponent {...this.props} />;
}
};
}
const EnhancedButton = withLogger(Button);
Render Props 版本
function Logger({ children }) {
return children({
logMount: () => console.log('Component mounted'),
logUnmount: () => console.log('Component will unmount')
});
}
// 使用
<Logger>
{({ logMount, logUnmount }) => (
<Button onMount={logMount} onUnmount={logUnmount} />
)}
</Logger>
Hook 版本
function useLogger(name) {
useEffect(() => {
console.log(`${name} mounted`);
return () => console.log(`${name} will unmount`);
}, [name]);
}
function Button() {
useLogger('Button');
return <button>Click</button>;
}
结论:Hook 版本最简洁,没有额外的组件嵌套。
6. 何时选哪种模式
决策树
需要复用逻辑吗?
│
├─ 是 → 用 Hook(2019+ 首选)
│ ├─ 简单状态逻辑 → useState + useEffect
│ ├─ 复杂状态 → useReducer
│ └─ 跨组件共享 → Context + useContext
│
└─ 否 → 继续用普通组件
特殊情况
| 场景 | 推荐 |
|---|---|
| 旧项目(React < 16.8) | HOC 或 Render Props |
| 需要劫持组件生命周期 | HOC(可以访问 WrappedComponent 的生命周期) |
| 库作者(需要兼容多种用法) | Render Props(更灵活) |
| 新项目 | Hooks |
为什么 Hooks 是现代首选
- 没有嵌套地狱:
withAuth(withTheme(withData(Component)))变成useAuth()+useTheme()+useData() - 没有 this 指向问题:函数组件 + Hooks 完全不用关心 this
- 类型推断好:TypeScript 对 Hooks 的支持更完善
- 测试简单:直接调用 Hook 函数,不用 mount 组件树
7. 总结
| 模式 | 核心思想 | 适用场景 |
|---|---|---|
| HOC | 函数转换组件 | 需要转换/增强组件的场景 |
| Render Props | 函数控制渲染 | 需要动态决定渲染内容的场景 |
| Hooks | 函数复用带状态逻辑 | 现代 React 首选 |
记忆点:
- HOC = 包装,Render Props = 注入,Hooks = 调用
- 遇到新场景,优先考虑 Hooks
- 老项目逐步迁移,不用一次性重写
相关技术栈:React 16.8+, Hooks, HOC, Render Props, TypeScript
如果你觉得这篇文章有帮助,欢迎关注,我会持续输出 React 进阶内容。
参考资料: