学完这篇你能拿到一套判断标准:遇到「多组件要共享同一段状态逻辑」时,知道该写高阶组件、Render Props 还是自定义 Hook,并且能避开这三种写法各自的经典坑。
第一步:先明白三种模式解决的是同一个问题
React 里组件复用的痛点从来不是「UI 长一样」,而是「逻辑一样」——监听鼠标位置、订阅 WebSocket、处理表单校验、根据登录态决定渲染什么。这三种模式都是把状态逻辑从组件里抽出来,让多个组件共用。区别只在于抽出来的形式不同:HOC 抽成「包一层组件」,Render Props 抽成「传一个函数」,Hook 抽成「一个函数调用」。
理解这一点,后面选型就不会乱。
第二步:高阶组件 HOC——接收组件,返回新组件
HOC 本质是一个函数:入参是组件,出参也是组件。
function withAuth(WrappedComponent) {
function WithAuth(props) {
const { user } = useAuth();
if (!user) return <Login />;
return <WrappedComponent {...props} user={user} />;
}
WithAuth.displayName = `withAuth(${WrappedComponent.displayName || WrappedComponent.name})`;
return WithAuth;
}
const Profile = withAuth(ProfileBase);
它适合做横切关注点:权限拦截、埋点上报、错误边界包裹、给组件注入统一的 props。因为它在组件树上是真实的一层,所以能拦截渲染、能决定要不要渲染子组件——这是 Hook 做不到的。
注意:别在组件的 render 里调用 HOC,比如 render() { const X = withAuth(Foo); return <X /> }。每次渲染都会生成一个全新的组件类型,React 会把它当成不同组件,直接卸载重建,输入框失焦、状态全丢。HOC 要在模块顶层调用一次。
注意:HOC 默认不透传 ref,也不带原组件的静态方法。传统写法需要 forwardRef 包一层,静态方法用 hoist-non-react-statics 复制过去(React 19 里 ref 已作为普通 prop 传递,这条约束弱化了,但老项目仍要注意)。
第三步:Render Props——把「渲染什么」交给调用方
组件自己管状态,把状态通过一个函数 prop 交出去:
function Mouse({ render }) {
const [pos, setPos] = useState({ x: 0, y: 0 });
useEffect(() => {
const onMove = (e) => setPos({ x: e.clientX, y: e.clientY });
window.addEventListener('mousemove', onMove);
return () => window.removeEventListener('mousemove', onMove);
}, []);
return render(pos);
}
// 使用
<Mouse render={({ x, y }) => <p>鼠标在 {x}, {y}</p>} />
用 children 当函数写也一样,<Mouse>{({x}) => ...}</Mouse> 更常见。它的价值在于:状态在内部,UI 完全由外部决定,不产生额外的组件层级包裹问题。
注意:如果被包裹的组件是 React.memo / PureComponent,父组件每次渲染都会新建一个内联箭头函数,props 引用变了,浅比较失效,等于白优化。必要时用 useCallback 固定这个函数。
注意:别把 Render Props 套三层以上,回调里再套回调,读起来比回调地狱还难受。这种情况改用 Hook。
第四步:自定义 Hook——目前的首选
React 16.8 之后,同样的逻辑可以写成普通函数:
function useMouse() {
const [pos, setPos] = useState({ x: 0, y: 0 });
useEffect(() => {
const onMove = (e) => setPos({ x: e.clientX, y: e.clientY });
window.addEventListener('mousemove', onMove);
return () => window.removeEventListener('mousemove', onMove);
}, []);
return pos;
}
function App() {
const { x, y } = useMouse();
return <p>鼠标在 {x}, {y}</p>;
}
没有额外组件层级,没有嵌套回调,返回值随便解构,多个 Hook 还能自由组合。今天写新代码,逻辑复用基本都应该先考虑它。
注意:Hook 只能在组件或另一个 Hook 的顶层调用,不能放进 if、循环、try/catch 里。React 靠调用顺序匹配状态,顺序一变就全错位。
注意:函数名必须以 use 开头。eslint-plugin-react-hooks 的检查规则完全依赖这个前缀,不写 use 它就不会帮你校验调用位置,埋雷。
注意:两个组件调用同一个自定义 Hook,状态是互相独立的,不共享。要共享得靠 Context 或状态库,别指望 Hook 当全局 store。
第五步:怎么选
- 需要拦截渲染 / 决定是否渲染子组件(权限、错误边界、Suspense 兜底)→ HOC。
- 需要把状态交给调用方、且必须在组件树里显式占位(比如配合某些只接受 children 的第三方库)→ Render Props。
- 其余所有「抽状态逻辑」的场景 → 自定义 Hook,优先用它。
老项目里三种可能同时存在,不必强行重构,新代码按上面选就行。
小结
- 三者目标一致:复用状态逻辑,不是复用 UI。
- HOC:函数收组件返组件,适合横切关注点;顶层调用、注意 ref 与静态方法。
- Render Props:状态在内、渲染在外;注意内联函数导致的 memo 失效和嵌套过深。
- 自定义 Hook:无额外层级、可组合,是当下的默认选择;守好「顶层调用」和「use 前缀」两条规矩。
- 选型口诀:要拦渲染找 HOC,要交渲染找 Render Props,其余上 Hook。