mcpField
H
hoc-pattern

hoc-pattern

github.com/patternsdev开发2571.0k 次安装28 周前更新
代码仓库

关于 hoc-pattern

介绍高阶组件及其在 React 中复用组件逻辑的方式。面向需要共享行为的开发者,涵盖 HOC 组合、属性冲突及与 Hooks 的取舍。

主要功能

  1. 01为组件包装可复用行为
  2. 02组合多个 HOC
  3. 03向被包装组件传递数据和属性
  4. 04避免属性名称冲突
  5. 05比较 HOC 与 React Hooks

适用场景

  1. 01为多个组件应用共享样式
  2. 02复用授权或全局状态逻辑
  3. 03为数据驱动组件添加加载行为
  4. 04评估现有 React 代码中的 HOC

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/patternsdev/skills --skill hoc-pattern

安装后在 Claude Code 中输入/hoc-pattern确认已加载。

详细介绍

HOC 模式

目录

在应用中,我们经常需要在多个组件中使用相同的逻辑。这些逻辑可以包括为组件应用特定样式、要求授权或添加全局状态。

复用多个组件中相同逻辑的一种方式是使用高阶组件模式。这种模式允许我们在整个应用中复用组件逻辑。

适用场景

  • 当需要为多个组件应用相同且未经自定义的行为时,使用此模式
  • 当组件需要在不添加自定义逻辑的情况下独立运行时,此模式很有用

不适用场景

  • 当自定义 Hook 能以更少的嵌套和更好的可读性实现相同效果时
  • 在新的 React 18+ 代码中,Hook 是共享有状态逻辑的惯用方式
  • 当 HOC 包装器导致属性名称冲突,或使 DevTools 中的组件树难以辨认时

说明

  • 创建一个接收组件并返回具有增强行为的新组件的函数
  • 在 HOC 中重命名或合并属性,避免名称冲突
  • 对于大多数新代码,优先使用 React Hooks 而非 HOC,以避免包装器地狱和深层嵌套
  • 谨慎组合多个 HOC,并注意组合顺序很重要

详情

高阶组件(HOC)是一个接收另一个组件的组件。HOC 包含我们想要应用于作为参数传入的组件的特定逻辑。应用该逻辑后,HOC 会返回添加了额外逻辑的元素。

假设我们总是想为应用中的多个组件添加特定样式。我们不必每次都在本地创建 style 对象,而是可以创建一个 HOC,将 style 对象添加到传入的组件中:

function withStyles(Component) {
  return props => {
    const style = { padding: '0.2rem', margin: '1rem' }
    return <Component style={style} {...props} />
  }
}

const Button = () => <button>Click me!</button>
const Text = () => <p>Hello World!</p>

const StyledButton = withStyles(Button)
const StyledText = withStyles(Text)

我们刚刚创建了 StyledButton 和 StyledText 组件,它们是修改后的 Button 和 Text 组件。现在,这两个组件都包含了在 withStyles HOC 中添加的样式!

让我们稍微改善一下用户体验。获取数据时,我们想向用户显示一个 "Loading..." 界面。我们不必直接向 DogImages 组件添加数据,而是可以使用一个高阶组件来为我们添加这段逻辑。

让我们创建一个名为 withLoader 的 HOC。HOC 应接收一个组件,并返回该组件。在本例中,withLoader HOC 应接收一个元素,该元素会在数据获取完成前显示 Loading…。

function withLoader(Element) {
  return (props) => <Element />;
}

不过,我们并不只是想返回它接收到的元素,而是想让该元素包含逻辑,用来判断数据是否仍在加载。

为了让 withLoader HOC 更具可复用性,我们不会在该组件中硬编码 Dog API 的 URL。相反,我们可以将 URL 作为参数传递给 withLoader HOC,这样,在从不同 API 端点获取数据时,任何需要加载指示器的组件都可以使用这个加载器。

function withLoader(Element, url) {
  return (props) => {};
}

HOC 会返回一个元素;在本例中,它是一个函数组件 props => {}。我们想为它添加逻辑,以便在数据仍在获取时显示 Loading… 文本。数据获取完成后,组件应将获取到的数据作为 prop 传递。

我们刚刚创建了一个可以接收任意组件和 URL 的 HOC。

  1. 在 useEffect hook 中,withLoader HOC 会从 API 端点获取数据,该端点作为 url 的值传入。数据尚未返回时,我们会返回包含 Loading... 文本的元素。
  2. 数据获取完成后,我们将 data 设置为获取到的数据。由于 data 不再是 null,我们就可以显示传递给 HOC 的元素了!

那么,如何在应用中添加这一行为呢?在 DogImages.js 中,我们不再只导出原始的 DogImages 组件,而是要导出由 withLoading HOC 包装后的 DogImages 组件。

export default withLoader(
  DogImages,
  "https://dog.ceo/api/breed/labrador/images/random/6"
);

高阶组件模式让我们能够为多个组件提供相同的逻辑,同时将所有逻辑集中在一个地方。withLoader HOC 不关心接收到的组件或 URL 是什么:只要它们是有效的组件和 API 端点,它就会将该 API 端点返回的数据传递给传入的组件。

组合

我们还可以组合多个高阶组件。假设我们还想添加一项功能:当用户将鼠标悬停在 DogImages 列表上时,显示一个 Hovering! 文本框。

我们需要创建一个 HOC,为传入的元素提供 hovering prop。根据该 prop,我们可以有条件地渲染文本框,以反映用户是否正将鼠标悬停在 DogImages 列表上。

现在,我们可以将 withHover HOC 包装在 withLoader HOC 外面。

现在,DogImages 元素包含了从 withHover 和 withLoader 传入的所有 props。我们可以根据 hovering prop 的值是 true 还是 false,有条件地渲染 Hovering! 文本框。

一个用于组合 HOC 的知名库是 recompose。由于 HOC 在很大程度上可以由 React Hooks 替代,recompose 库已不再维护。

Hooks

在某些情况下,我们可以用 React Hooks 替代 HOC 模式。

让我们将 withHover HOC 替换为 useHover Hook。我们不再使用高阶组件,而是导出一个 Hook,为元素添加 mouseOver 和 mouseLeave 事件监听器。我们不能再像使用 HOC 时那样传入元素,而是会从 Hook 返回一个 ref,它应该接收 mouseOver 和 mouseLeave 事件。

useEffect Hook 会为组件添加事件监听器,并根据用户当前是否将鼠标悬停在元素上,将 hovering 的值设为 true 或 false。Hook 需要同时返回 ref 和 hovering:ref 用于为应接收 mouseOver 和 mouseLeave 事件的组件添加 ref,hovering 则用于有条件地渲染 Hovering! 文本框。

我们可以直接在 DogImages 组件中使用 useHover Hook,而不是用 withHover 组件包装它。

一般来说,React Hooks 并不会取代 HOC 模式。

“在大多数情况下,Hooks 已经足够使用,并且有助于减少组件树中的嵌套。” - React 文档

正如 React 文档所述,使用 Hooks 可以减少组件树的深度。采用 HOC 模式时,组件树很容易变得层层嵌套。

<withAuth>
  <withLayout>
    <withLogging>
      <Component />
    </withLogging>
  </withLayout>
</withAuth>

直接为组件添加 Hook 后,我们就不再需要包装组件了。

高阶组件可以让多个组件使用相同的逻辑,同时将这些逻辑集中在一处。Hooks 则允许我们在组件内部添加自定义行为。如果多个组件都依赖这种行为,与 HOC 模式相比,这可能会增加引入 bug 的风险。

HOC 的最佳适用场景:

  • 应用中的多个组件都需要使用_相同且未经定制的_行为。
  • 组件无需添加自定义逻辑也能独立运行。

Hooks 的最佳适用场景:

  • 使用该行为的每个组件都需要对其进行定制。
  • 该行为不会在整个应用中普遍使用,只有一个或少数几个组件会用到它。
  • 该行为会为组件添加许多属性。
案例研究

一些依赖 HOC 模式的库在 Hooks 发布后添加了 Hooks 支持。Apollo Client 就是一个很好的例子。

使用 Apollo Client 的一种方式是通过 graphql() 高阶组件。借助 graphql() HOC,我们可以将客户端中的数据提供给由该高阶组件包装的组件!虽然目前我们仍然可以使用 graphql() HOC,但这样做有一些缺点。

当组件需要访问多个解析器时,我们需要组合多个 graphql() 高阶组件才能实现。组合多个 HOC 会让人难以理解数据是如何传递给组件的。在某些情况下,HOC 的顺序很重要,因此重构代码时很容易引入 bug。

Hooks 发布后,Apollo 为 Apollo Client 库添加了 Hooks 支持。开发者现在可以通过该库提供的 Hooks 直接访问数据,而不必使用 graphql() 高阶组件。

通过使用 useMutation Hook,我们减少了向组件提供数据所需的代码量。

除了减少样板代码,在组件中使用多个解析器的数据也容易得多。我们不必组合多个高阶组件,只需在组件中编写多个 Hook 即可。这样更容易了解数据是如何传递到组件的,也能在重构组件或将其拆分为更小的部分时提升开发体验。

优点

使用高阶组件模式,可以将需要复用的逻辑集中在一处。这样可以降低因反复复制代码而将错误扩散到整个应用、并可能每次都引入新错误的风险。将逻辑集中在一处,有助于保持代码 DRY,并轻松落实关注点分离。

缺点

高阶组件传递给元素的属性名称可能会发生命名冲突。

function withStyles(Component) {
  return props => {
    const style = { padding: '0.2rem', margin: '1rem' }
    return <Component style={style} {...props} />
  }
}

const Button = () => <button style={{ color: 'red' }}>Click me!</button>
const StyledButton = withStyles(Button)

在这种情况下,withStyles 高阶组件会向传入的元素添加一个名为 style 的属性。不过,Button 组件本身已经有一个名为 style 的属性,这个属性会被覆盖!请确保高阶组件能够处理意外的命名冲突,可以重命名属性,也可以合并属性。

function withStyles(Component) {
  return props => {
    const style = {
      padding: '0.2rem',
      margin: '1rem',
      ...props.style
    }

    return <Component style={style} {...props} />
  }
}

const Button = () => <button style={{ color: 'red' }}>Click me!</button>
const StyledButton = withStyles(Button)

如果使用多个组合的高阶组件,并且它们都会向内部包装的元素传递属性,就可能难以判断每个属性分别由哪个高阶组件负责。这会妨碍调试,也会让应用难以扩展。

来源

参考资料

评论

支持 Markdown