--- tags: [React, HOC, Render Props, Pattern, Frontend] create time: 2026-04-29 22:11 --- # HOC 与 Render Props ## 概述 > [!question] 思考:如果两个组件需要共享同一套数据获取逻辑,该怎么设计? Hooks 出现之前,HOC(高阶组件)和 Render Props 是 React 中**复用状态逻辑**的两种主要模式。理解它们的原理、适用场景和局限,不仅有助于阅读遗留代码,更能帮你深刻理解为什么 Hooks 成为更优解——以及哪些场景下旧模式依然不可替代。 | 维度 | HOC | Render Props | Custom Hook | |------|-----|-------------|-------------| | **本质** | 函数包裹组件 | prop 传递渲染回调 | 纯函数抽离逻辑 | | **侵入性** | 不改动原组件 JSX | 需修改 JSX 调用处 | 零侵入,仅在 hooks 内使用 | | **性能影响** | 额外 wrapper 层 | 同上 | **无额外组件树开销** | ## HOC(高阶组件) ### 概念 > [!question] 类比理解:如果把组件看作"数据 → UI"的转换器,HOC 就是在输入输出之间插入了一层"调料"。 ```mermaid graph LR A["原始组件 Component"] -->|"注入 props"| B["HOC 函数"] B --> C["增强组件 EnhancedComponent"] C --> D["获得额外能力:日志/权限/数据"] style B fill:#F5A87D,color:#000 ``` HOC 是一个**函数**,接收组件作为参数,返回增强后的新组件: ```tsx // 基础模式 function withAuth

( WrappedComponent: React.ComponentType

) { return function WithAuth(props: P) { const user = useAuth(); if (!user) return ; return ; }; } // 使用:装饰器风格 const ProtectedPage = withAuth(function Dashboard() { return

Dashboard

; }); // TS 类型推导:保留原始 props interface Props { title: string } const StyledTitle = withTheme(function Title({ title }: Props) { return

{title}

; }); // ✅ TS 推断 Props 不变 ``` > [!note] 核心原理 > HOC 本质上是**闭包 + 组合**。`WithAuth` 作为一个新的函数组件,可以正常使用 Hooks(因为它是组件),而包裹的 `WrappedComponent` 只负责渲染。这种方式**不修改原组件的任何代码**,仅通过 props 传递增强信息。 #### 关键细节:displayName 调试嵌套 HOC 时,React DevTools 会显示一层层无意义的 wrapper 名。可以用 `displayName` 让调试更清晰: ```tsx export function withAuth

(Wrapped: React.ComponentType

) { const WithAuth: React.FC

= (props) => { // ... 认证逻辑 return ; }; WithAuth.displayName = `withAuth(${Wrapped.name || 'Component'})`; return WithAuth; } // DevTools 中显示:withAuth(Dashboard) → 一目了然 ``` ### 常见 HOC 模式 #### 1. 日志 / HUD ```tsx // 通用:为组件自动记录生命周期事件 function withLogger

(Comp: React.FC

): React.FC

{ return function LoggedComponent(props: P) { useEffect(() => console.log(`[Mount] ${Comp.name || 'Anonymous'}`), []); return ; }; } ``` > [!tip] 解释 > 不传任何额外 prop,只"旁路"添加副作用(如日志)。这是 HOC 最轻量的用法——原组件完全不知情。 #### 2. 加载态封装 ```tsx // 自动管理 loading 状态 + 错误处理 function withLoading

( Comp: React.FC

, fetchData: () => Promise ): React.FC

{ return function LoadingWrapper(props: P) { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { fetchData().then(() => setLoading(false)).catch(setError); }, []); if (error) return ; return loading ? : ; }; } ``` > [!note] 解释 > HOC 在**不改动组件渲染逻辑**的前提下,统一接管了 "加载中 / 出错 / 成功" 三种 UI 状态。多个页面复用同一套加载策略,DRY。 #### 3. Props 转换 ```tsx // 将 props 做格式变换后注入子组件 function withPropsTransformer

( Comp: React.FC

, transform: (p: P) => Record ): React.FC { return function Transformed(props: P) { const extra = transform(props); // ⚠️ transform 返回的 key 可能与原 props 冲突 return ; }; } ``` > [!warning] 注意 > 当 `transform` 返回的 key 和原 props 同名时,后者会覆盖前者。实际项目中建议约定命名空间前缀,例如 `apiData`、`cacheMeta` 等。 ### HOC 的局限性 | 问题 | 说明 | 解决方案 | |------|------|---------| | **Static 属性丢失** | `withAuth(Page)` 返回的新组件没有 Page.getInitialProps | 手动拷贝:`EnhancedComponent.getInitialProps = Wrapped.getInitialProps` | | **Wrapper Hell** | `withAuth(withLogging(withData(Page)))`——嵌套过深调试困难 | 用 `compose` 函数扁平化,或直接改用 Custom Hook | | **Props 冲突** | 多个 HOC 都注入 `data` prop,后一个覆盖前一个 | 使用唯一命名空间前缀(如 `_data` → `useData.data`) | | **ref 丢失** | 直接传 ref 给增强组件会报错(需用 forwardRef 包装) | 见下方兼容 ref 的方案 | | **隐式依赖** | HOC 内部调用的 hook 不透明,阅读者不知道注入了什么 | 规范命名(`withXxx`),文档说明行为 | #### 为什么需要 forwardRef? ```tsx export function withAuth

(Wrapped: React.ComponentType

) { return React.forwardRef((props, ref) => { const { authenticated } = useAuth(); if (!authenticated) return ; return ; }); } ``` > [!tip] 解释 > 普通函数组件不能接收 `ref`。当 HOC 需要透传 `ref` 给子组件时,必须用 `React.forwardRef` 包裹,否则 React 会抛出 "Function components cannot be given refs" 错误。这增加了额外的样板代码——Hooks 天然解决了这个问题。 #### compose 工具 当多层 HOC 叠加时,`compose` 可以让可读性更好: ```tsx // before: 从内到外,阅读时要先找最内层 const Enhanced = withAuth(withCache(withPagination(UserList))); // after: 从左到右,符合阅读习惯 const Enhanced = compose( withAuth, withCache, withPagination, )(UserList); ``` > [!question] 思考 > compose 真的改善了可读性吗?如果每个 HOC 的作用一目了然,你是否还需要它? ## Render Props ### 核心思想 > [!question] 和 HOC 相比,Render Props 到底"额外"给了什么能力? 通过 prop 传递一个**函数**,该函数返回 JSX——将 UI 渲染逻辑委托给调用方。 与 HOC 的本质区别:HOC 通过**组件嵌套**注入 props;Render Props 通过**函数传参**直接暴露状态。 ```tsx interface MouseTrackerProps { render: (position: { x: number; y: number }) => React.ReactNode; } function MouseTracker({ render }: MouseTrackerProps) { const [pos, setPos] = useState({ x: 0, y: 0 }); useEffect(() => { const handler = (e: MouseEvent) => setPos({ x: e.clientX, y: e.clientY }); window.addEventListener("mousemove", handler); return () => window.removeEventListener("mousemove", handler); }, []); // 🎯 关键:render 函数返回什么就渲染什么 // 状态在父组件,UI 由调用方决定 return

{render(pos)}
; } // 使用 (
Cursor at ({x}, {y})
)} />; ``` > [!note] 解释 > `MouseTracker` 是**纯粹的关注点分离**:它只负责追踪鼠标位置并触发重渲染,完全不关心屏幕上画什么。这种"关注点解耦"是 HOC 难以优雅表达的。 ### children 作为 Render Prop 当 render prop 只是把数据传给子内容时,可以用 `children`(本身也是函数)替代——语义更自然。 ```tsx // children prop 的类型:接收 data,返回 ReactNode function DataProvider({ children }: { children: (data: Data) => React.ReactNode }) { const data = useDatabase(); return <>{children(data)}; } {(data) => } ; ``` > [!tip] children vs render > - **用 children**:渲染逻辑简单、只需要传入数据 → 代码最简洁 > - **用 render prop**:需要多个渲染入口(如 `renderEmpty` / `renderLoading`)、或需要对渲染做参数校验 → 类型约束更精确 ### 常见模式:多回调拆分 当渲染逻辑复杂时,可以将一个大 `render` 拆成多个小 prop,降低心智负担: ```tsx interface CarouselProps { slides: Slide[]; renderItem: (slide: Slide, index: number) => React.ReactNode; renderIndicator?: (index: number, active: boolean) => React.ReactNode; } function Carousel({ slides, renderItem, renderIndicator }: CarouselProps) { const [active, setActive] = useState(0); return (
{slides.map((s, i) => renderItem(s, i))}
{slides.map((_, i) => renderIndicator?.(i, i === active) ?? ( setActive(i)} className={i === active ? 'dot-active' : 'dot'} /> ))}
); } ``` > [!note] 为什么拆比合好? > 一个巨大的 `render` 回调会让调用处变得冗长。拆成独立的小 prop(`renderItem`、`renderIndicator`),调用方**按需实现**,未实现的可以省略——这是 React 库设计的经典模式。 ## HOC vs Render Props vs Custom Hook ```mermaid graph TB A["逻辑复用需求"] --> B["三种方案"] B --> C["HOC
函数包裹组件"] B --> D["Render Props
函数传递 prop"] B --> E["Custom Hook
纯函数抽离逻辑"] C --> C1["⚠️ Wrapper 嵌套深"] C --> C2["⚠️ 静态方法丢失"] C --> C3["⚠️ ref 需 forwardRef"] C --> C4["✅ 不修改原组件 JSX"] D --> D1["⚠️ 回调嵌套过深"] D --> D2["⚠️ Prop 命名冲突风险"] D --> D3["✅ UI 控制权完全交给调用方"] E --> E1["✅ 扁平可读"] E --> E2["✅ 直接操作 state / effect"] E --> E3["✅ 无额外组件树开销"] E --> E4["❌ 只能在组件内部使用"] style C4 fill:#4FC08D,color:#fff style D3 fill:#4FC08D,color:#fff style E1 fill:#4FC08D,color:#fff style E2 fill:#4FC08D,color:#fff style E3 fill:#4FC08D,color:#fff ``` ## 为什么 Hooks 取代了它们? Hooks 的核心理念是**把状态逻辑从组件中抽离出来,但保持调用处扁平**。它结合了 HOC 和 Render Props 的优点: | 维度 | HOC / Render Props | Custom Hook | |------|-------------------|-------------| | **嵌套层级** | 多层 wrapper 或回调嵌套 | **零嵌套,直接平铺** | | **状态共享** | 需要 prop 层层传递,容易混乱 | 每个 hook 管理自己的 state,互不干扰 | | **TypeScript** | 泛型约束复杂,组合后类型可能丢失 | **完美推断**,不需要额外 type wrangling | | **调试** | DevTools 多一层 wrapper,栈更混乱 | 与普通函数无异 | ```tsx // ❌ HOC 方式:三层嵌套 → 调试困难、性能不可见、type 推导混乱 const ConnectedUserList = withAuth(withCache(withPagination(UserList))); // ✅ Hook 方式:扁平可读,逻辑一目了然 function UserList() { useAuth(); // 身份验证 const cache = useCache(); // 数据缓存 const pagination = usePagination(); // 分页管理 return
{/* ... */}
; } ``` > [!question] 思考 > Hooks 真的完全淘汰了 HOC 和 Render Props 吗?回想一下文档开头的两个核心概念——HOC 通过**包裹**增强组件,Render Props 通过**回调**暴露渲染控制。这两种模式在哪些场景中仍然无法被 Hook 替代? #### 何时仍应使用旧模式? | 场景 | 推荐方案 | 原因 | |------|---------|------| | 第三方库封装(不接触用户代码) | **HOC** | 库方只操作组件类,无需知道用户内部结构 | | UI 框架需要提供渲染占位符 | **Render Props** | 父组件需要定义多个渲染回调(如 `renderEmpty`、`renderLoading`) | | 通用业务逻辑复用 | **Custom Hook** | 首选方案:简洁、可组合、无 DOM 开销 | ## 关联笔记 - [[00.Readme]] - [[3. Hooks 篇/01-为什么需要 Hooks]] - [[3. Hooks 篇/02-useEffect 深度解析]] - [[3. Hooks 篇/08-自定义 Hook 最佳实践]] - [[4. 进阶篇/07-forwardRef 与 useImperativeHandle]] - [[4. 进阶篇/11-组件 Composition 模式]]