React 组件设计模式:高阶组件、Render Props 与 Hooks 对比
React 组件设计模式:高阶组件、Render Props 与 Hooks 对比
一、Introduction
React 有多种代码复用模式,不同模式下代码的组织方式、可复用逻辑的形态、以及性能开销都不同。在 Hooks 出现之前,高阶组件(HOC)和 Render Props 是两种主流的逻辑复用方式;Hooks 出现后,很多场景用 Hooks 更简洁。
但这不意味着 HOC 和 Render Props 已经被淘汰——它们在某些场景下仍有独特价值。本文用实战案例讲解三种模式的用法、优缺点,以及在什么场景下该选哪种。
---
二、Render Props 模式
#### 2.1 核心思想
Render Props 的核心是"把渲染逻辑作为 prop 传入组件",让父组件控制子组件的渲染内容,同时复用逻辑层的代码:
import React, { useState, useEffect } from 'react';
// ==================== Render Props: 鼠标位置追踪 ====================
class MouseTracker extends React.Component {
state = { x: 0, y: 0 };
handleMouseMove = (e) => {
this.setState({ x: e.clientX, y: e.clientY });
};
componentDidMount() {
window.addEventListener('mousemove', this.handleMouseMove);
}
componentWillUnmount() {
window.removeEventListener('mousemove', this.handleMouseMove);
}
render() {
// 把状态作为 props 传给 render 函数
return this.props.render(this.state);
}
}
// 使用方:完全自定义渲染方式
function MouseTrackerDemo() {
return (
<div>
<h3>Render Props 示例</h3>
<MouseTracker
render={({ x, y }) => (
<div>
<p>鼠标位置:({x}, {y})</p>
<div
style={{
width: 20,
height: 20,
borderRadius: '50%',
background: 'red',
position: 'fixed',
left: x - 10,
top: y - 10,
pointerEvents: 'none'
}}
/>
</div>
)}
/>
</div>
);
}
// ==================== Render Props: 数据请求复用 ====================
class DataFetcher extends React.Component {
state = { data: null, loading: true, error: null };
componentDidMount() {
this.fetchData();
}
componentDidUpdate(prevProps) {
if (prevProps.url !== this.props.url) {
this.fetchData();
}
}
fetchData = async () => {
this.setState({ loading: true, error: null });
try {
const response = await fetch(this.props.url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
this.setState({ data, loading: false });
} catch (err) {
this.setState({ error: err.message, loading: false });
}
};
render() {
return this.props.render(this.state);
}
}
function PostsList() {
const [postId, setPostId] = useState(1);
return (
<div>
<h3>数据请求复用示例</h3>
<button onClick={() => setPostId(id => Math.max(1, id - 1))}>上一篇</button>
<span style={{ margin: '0 12px' }}>第 {postId} 篇</span>
<button onClick={() => setPostId(id => id + 1)}>下一篇</button>
<DataFetcher
url={`https://jsonplaceholder.typicode.com/posts/${postId}`}
render={({ data, loading, error }) => {
if (loading) return <p>加载中...</p>;
if (error) return <p>错误:{error}</p>;
return (
<div style={{ marginTop: 12 }}>
<h4>{data.title}</h4>
<p>{data.body}</p>
</div>
);
}}
/>
</div>
);
}
---
三、高阶组件(HOC)模式
#### 3.1 核心思想
HOC 是一个函数,接收一个组件作为参数,返回一个增强了的新组件。它通过包装原组件,在原组件的外层添加额外的功能:
import React from 'react';
// ==================== HOC: 权限控制 ====================
// withAuth:检查用户是否已登录,未登录则重定向
function withAuth(WrappedComponent) {
return function AuthenticatedComponent(props) {
const [isAuthenticated, setIsAuthenticated] = React.useState(false);
const [isLoading, setIsLoading] = React.useState(true);
React.useEffect(() => {
// 模拟检查登录状态
const checkAuth = () => {
const token = localStorage.getItem('auth_token');
setIsAuthenticated(!!token);
setIsLoading(false);
};
checkAuth();
}, []);
if (isLoading) return <div>检查登录状态中...</div>;
if (!isAuthenticated) {
return <div>🔒 请先登录后再访问此页面</div>;
}
// 把原 props 透传给被包装的组件
return <WrappedComponent {...props} />;
};
}
// ==================== HOC: 添加 Loading 状态 ====================
function withLoading(WrappedComponent) {
return function WithLoadingComponent({ isLoading, loadingIndicator, ...props }) {
if (isLoading) {
return loadingIndicator || <div style={{ padding: 20, textAlign: 'center' }}>⏳ 加载中...</div>;
}
return <WrappedComponent {...props} />;
};
}
// ==================== HOC: 控制台日志(调试用)====================
function withLogger(WrappedComponent) {
return function LoggerComponent(props) {
React.useEffect(() => {
console.log(`[${WrappedComponent.name}] 挂载`);
return () => console.log(`[${WrappedComponent.name}] 卸载`);
}, []);
console.log(`[${WrappedComponent.name}] 渲染,props:`, props);
return <WrappedComponent {...props} />;
};
}
// ==================== 使用示例 ====================
class OriginalDashboard extends React.Component {
render() {
return (
<div>
<h2>仪表盘(class 组件)</h2>
<p>当前用户:{this.props.username}</p>
<p>用户等级:{this.props.userLevel}</p>
</div>
);
}
}
// 用 HOC 包装
const DashboardWithAuth = withAuth(OriginalDashboard);
const DashboardWithLoading = withLoading(DashboardWithAuth);
const DashboardLogged = withLogger(DashboardWithLoading);
// 模拟使用(username 和 userLevel 是 HOC 透传下来的 props)
function HOCDemo() {
const [isLoading, setIsLoading] = React.useState(true);
React.useEffect(() => {
setTimeout(() => setIsLoading(false), 2000);
}, []);
return (
<div>
<h3>HOC 示例(2秒后显示仪表盘)</h3>
<DashboardLogged
isLoading={isLoading}
username="管理员"
userLevel={99}
/>
</div>
);
}
---
四、三种模式对比与选择指南
| 维度 | Render Props | HOC | Hooks |
|------|-------------|-----|-------|
| 学习成本 | 中等 | 较高 | 低 |
| 代码量 | 少(嵌套少时) | 多(需要包装) | 最少 |
| Props 冲突 | 无(显式传递) | 有(props 命名冲突) | 无 |
| 静态组合 | 困难 | 容易 | 容易 |
| 适用场景 | 状态复用+自定义渲染 | 横切关注点(权限、日志) | 逻辑复用(推荐优先用) |
选择建议:
- 优先用 <strong>Hooks</strong>:大多数逻辑复用场景,Hooks 更直观
- 用 <strong>Render Props</strong>:当需要让使用者自定义渲染逻辑时
- 用 <strong>HOC</strong>:当需要在组件挂载/卸载时注入横切逻辑时
---
五、运行效果说明
MouseTrackerDemo:在页面上移动鼠标,红色圆点跟随鼠标移动,坐标实时显示。Render Props 把鼠标坐标暴露给使用方,使用方完全控制渲染方式——可以是坐标文字、可以是跟随圆点、可以是任何自定义 UI。
PostsList:点击"上一篇/下一篇"切换文章 ID,DataFetcher 自动发起新请求,显示加载状态,请求完成后显示文章标题和正文。
HOCDemo:DashboardWrapped 组件在 2 秒内显示加载状态(withLoading),2 秒后显示仪表盘内容(withAuth 控制是否允许访问),同时控制台打印挂载/卸载/渲染日志(withLogger)。
---
六、常见问题(FAQ)
#### Q1:Hooks 出来之后,HOC 和 Render Props 还有价值吗?
有价值,但应该优先用 Hooks。HOC 的独特价值在于:可以在 class 组件中使用(当你要给已有的 class 组件添加功能时),以及通过静态组合实现"装饰器"式的功能叠加。Render Props 在需要"状态共享 + 渲染自定义"时仍然优雅。
// Hooks 无法做到的事:给 class 组件添加功能
const EnhancedClassComponent = withAuth(ClassComponent); // HOC 可以做到
// 但给函数组件加功能,Hooks 是最优解
function FunctionalComponent() {
const { user } = useAuth(); // 清晰直观
}
#### Q2:HOC 会出现 Props 命名冲突吗?
是的,这是 HOC 最大的缺点。如果 HOC 没有正确地把 props 透传给被包装组件,或者多个 HOC 都试图设置同一个 prop 名称,就会产生冲突。解决方案:使用 hoist-non-react-statics 库自动复制静态方法,并确保所有非 React 的 props 都透传下去。
#### Q3:Render Props 会形成"嵌套地狱"吗?
会的。和 HOC 的"包装嵌套"问题类似,如果多个组件都使用 Render Props 模式,会形成多层 render 函数嵌套。Hooks 解决了这个问题——多个自定义 Hook 可以自由组合,不形成嵌套。
---
六、延伸阅读
- [React Hooks 完全指南:useState 与 useEffect 核心用法]()
- [React 自定义 Hooks 实战:逻辑复用与模块化]()
- [React Context API 状态管理深度理解]()
- [React 状态管理演进:从 useState 到 Redux]()
---