React 高阶组件(HOC)
高阶组件(HOC)本质是一个接收组件、返回增强后新组件的函数。名称来源于"高阶函数"——把组件当作参数传来传去,用于复用组件逻辑。
// 本质
;(Component) => NewComponent一、基本示例
下面是一个 withLoading HOC——根据 loading prop 决定显示加载态还是渲染目标组件:
// HOC 定义
function withLoading<P>(Component: React.ComponentType<P>) {
return function Wrapper(props: P & { loading?: boolean }) {
const { loading, ...rest } = props
if (loading) return <div>Loading...</div>
return <Component {...(rest as P)} />
}
}
// 使用
const UserPageWithLoading = withLoading(UserPage)
function Demo(params: type) {
return <UserPageWithLoading />
}React 社区约定用 with 前缀命名 HOC,语义上表示"给组件附加某种能力"——withAuth(认证)、withRouter(路由)、withTheme(主题)。
二、常见使用场景
HOC 在以下场景中广泛应用:
| 场景 | HOC | 注入内容 |
|---|---|---|
| 权限控制 | withAuth(Page) | 用户认证状态 |
| 路由能力 | withRouter() | history location 等 |
| 主题注入 | withTheme() | 当前主题配置 |
| 数据订阅 | withStore() | 全局 store 数据 |
| Loading 态 | withLoading() | loading 状态控制 |
本质上是对组件做切面增强(AOP):HOC 在原始组件外层包一层,注入额外的 props 或控制渲染逻辑。
三、HOC 的缺点
HOC 虽然灵活,但有四个明显的痛点:
- 嵌套地狱——多个 HOC 叠在一起:
withRouter(withTheme(withAuth(Component))),代码丑陋。 - props 来源不清晰——不知道哪个 HOC 注入了哪个 prop,排查困难。
- 命名冲突——多个 HOC 可能注入同名 prop(如
theme、user),互相覆盖。 - Wrapper Hell——React DevTools 里出现大量 Wrapper 组件,调试困难。
四、HOC vs Hooks
React 16.8 引入 Hooks 后,大部分 HOC 场景被替代:
| HOC | Hooks | |
|---|---|---|
| 实现方式 | 包装组件,注入 props | 直接在组件内部调用 |
| 嵌套 | 多层 Wrapper | 无额外组件嵌套 |
| props | 来源不透明,存在注入 | 显式调用,一目了然 |
| 适用 | 老项目、组件库 | 新项目,首选 |
// 以前 —— HOC
const Page = withRouter(UserPage) // props 被隐式注入
// 现在 —— Hooks
function UserPage() {
const navigate = useNavigate() // 显式获取
const theme = useTheme()
// ...
}五、TypeScript 常见写法
HOC 在 TS 中的核心难题是类型安全地注入和剥离 props。
1. 泛型 P 表示原组件 props
P 代表被包装组件自身需要的 props,HOC 在此基础上扩展。
2. 基础 TS 版本
最简单的带类型 HOC,在原 props 基础上混入新属性:
interface ThemeProps {
theme: 'light' | 'dark'
}
function withTheme<P>(Component: React.ComponentType<P & ThemeProps>) {
return function Wrapped(props: P) {
const theme = 'dark' as const
return <Component {...props} theme={theme} />
}
}3. 用 Omit 扣除注入 props(高级)
上面写法外部仍需手动传 theme。正确的做法是用 Omit 从 props 中剥离 HOC 注入的属性:
interface ThemeProps {
theme: string
}
function withTheme<P extends ThemeProps>(Component: React.ComponentType<P>) {
// 外部传入的 props 不再包含 theme
return function Wrapped(props: Omit<P, keyof ThemeProps>) {
const theme = 'dark'
return <Component {...(props as P)} theme={theme} />
}
}
// 外部不用再传 theme,HOC 已注入
const ThemeButton = withTheme(Button)
function Demo(params: type) {
return <ThemeButton text="submit" />
}4. 为什么用 React.ComponentType
React.ComponentType<P> 同时兼容函数组件和类组件,等价于 React.FC<P> | React.ComponentClass<P>。
六、面试总结
面试时可以把以下要点串起来回答:
HOC 本质是一个接收组件、返回增强后新组件的函数,用于复用组件逻辑。社区命名约定使用
with前缀,常见场景包括权限控制、路由注入、主题注入等。HOC 的缺点是多层嵌套、props 来源不透明和命名冲突。React Hooks 出现后,大部分 HOC 已被替代,但老项目和组件库中仍广泛使用。
TypeScript 中,HOC 通过泛型
P表示原组件 props,结合Omit可自动剥离注入的 props,避免外部重复传递。