FumadocsZDecode
React

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 虽然灵活,但有四个明显的痛点:

  1. 嵌套地狱——多个 HOC 叠在一起:withRouter(withTheme(withAuth(Component))),代码丑陋。
  2. props 来源不清晰——不知道哪个 HOC 注入了哪个 prop,排查困难。
  3. 命名冲突——多个 HOC 可能注入同名 prop(如 themeuser),互相覆盖。
  4. Wrapper Hell——React DevTools 里出现大量 Wrapper 组件,调试困难。

四、HOC vs Hooks

React 16.8 引入 Hooks 后,大部分 HOC 场景被替代:

HOCHooks
实现方式包装组件,注入 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,避免外部重复传递。

On this page