Published on

React设计模式 —— HOC模式

Authors
  • avatar
    Name
    Deng Hua
    Twitter

文章翻译至: Compound Pattern

目录

HOC组件

在我们的应用程序中,我们经常希望在多个组件中使用相同的逻辑。此逻辑可以包括向组件应用某种样式、需要授权或添加全局状态。

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

高阶组件 (HOC) 是接收另一个组件的组件。 HOC 包含我们想要应用于作为参数传递的组件的某些逻辑。应用该逻辑后,HOC 返回具有附加逻辑的元素。

假设我们一直想为应用程序中的多个组件添加某种样式。我们可以简单地创建一个 HOC,将 style 对象添加到我们传递给它的组件中,而不是每次都在本地创建 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)

我们刚刚创建了 StyledButtonStyledText 组件,它们是 ButtonText 组件的修改版本。它们现在都包含了 withStyles HOC 中添加的样式!

让我们看一下之前在容器/演示模式中使用的相同 DogImages 示例!该应用程序只是渲染从 API 获取的狗图像列表。

让我们稍微改善一下用户体验。当我们获取数据时,我们希望向用户显示一个 "Loading..."

我们可以使用高阶组件来为我们添加此逻辑,而不是直接向 DogImages 组件添加数据。

让我们创建一个名为 withLoader 的 HOC。 HOC 应该接收一个组件,并返回该组件。在这种情况下, withLoader HOC 应接收并显示 Loading… 的元素,直到获取数据完毕为止。

function withLoader(Element) {
  return (props) => <Element />; // 高阶组件返回的组件,可以劫持上一层传过来的props,然后混入新的props,来增强组件的功能
}

这是一个mini版本的HOC。然而,我们不仅仅想返回它收到的元素。相反,我们希望此元素包含告诉我们数据是否仍在加载的逻辑。

为了使 withLoader HOC 可复用,我们不会在该组件中硬编码 Dog API url。相反,我们可以将 URL 作为参数传递给 withLoader HOC,因此,此加载程序可用于任何需要从不同的 API 端点获取数据时显示加载指示符的组件。

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

HOC 返回一个元素,在本例中是一个函数组件 props => {} ,我们要向其中添加逻辑,允许我们在仍在获取数据时使用 Loading… 显示文本。获取数据后,组件应将获取的数据作为 prop 传递。

现在,在获取数据时我们会看到一个 Loading...

高阶组件模式允许我们向多个组件提供相同的逻辑,同时将所有逻辑保留在一个位置。 withLoader HOC 不关心它接收到的组件或 url

只要它是有效的组件和有效的 API url,它就会简单地将数据从该 API 传递到我们想要的组件。

组合

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

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

import React, { useState } from "react";

export default function withHover(Element) {
  return props => {
    const [hovering, setHover] = useState(false);

    return (
      <Element
        {...props}
        hovering={hovering}
        onMouseEnter={() => setHover(true)}
        onMouseLeave={() => setHover(false)}
      />
    );
  };
}

Hooks

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

useHover 钩子替换 withHover HOC。我们没有使用更高阶的组件,而是导出一个向元素添加 mouseOvermouseLeave 事件侦听器的钩子。

我们不能再像处理 HOC 那样传递该元素了。相反,我们将从hook返回 ref ,因为它应该获取 mouseOvermouseLeave 事件。

useHover.js

import { useState, useRef, useEffect } from "react";

export default function useHover() {
  const [hovering, setHover] = useState(false);
  const ref = useRef(null);

  const handleMouseOver = () => setHover(true);
  const handleMouseOut = () => setHover(false);

  useEffect(() => {
    const node = ref.current;
    if (node) {
      node.addEventListener("mouseover", handleMouseOver);
      node.addEventListener("mouseout", handleMouseOut);

      return () => {
        node.removeEventListener("mouseover", handleMouseOver);
        node.removeEventListener("mouseout", handleMouseOut);
      };
    }
  }, [ref.current]);

  return [ref, hovering];
}
import React from "react";
import withLoader from "./withLoader";
import useHover from "./useHover";

function DogImages(props) {
  const [hoverRef, hovering] = useHover();

  return (
    <div ref={hoverRef} {...props}>
      {hovering && <div id="hover">Hovering!</div>}
      <div id="list">
        {props.data.message.map((dog, index) => (
          <img src={dog} alt="Dog" key={index} />
        ))}
      </div>
    </div>
  );
}

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

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

在大多数情况下,Hooks 就足够了,并且有助于减少树中的嵌套。

正如 React 文档告诉我们的,使用 Hooks 可以减少组件树的深度。使用 HOC 模式,很容易得到深度嵌套的组件树。

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

通过直接给组件添加Hook,我们就不再需要包装组件了

使用高阶组件可以为许多组件提供相同的逻辑,同时将该逻辑全部保留在一个位置。钩子允许我们从组件内部添加自定义行为,如果多个组件依赖于此行为,则与 HOC 模式相比,这可能会增加引入错误的风险。

HOC 的最佳用例:

  • 整个应用程序中的许多组件需要使用相同的、未定制的逻辑。

  • 该组件可以独立工作,无需添加自定义逻辑。

优点

使用高阶组件模式允许我们将想要重用的逻辑保留在一个地方。这降低了通过一遍又一遍地复制代码而在整个应用程序中错误意外传播的风险,每次都可能引入新的错误。通过将逻辑全部保留在一个位置,我们可以保持代码的 "DRY" 并轻松实施关注点分离。

缺点

  1. HOC 可以传递给元素的 prop 的名称可能会导致命名冲突。
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 HOC 将一个名为 styleprop 添加到我们传递给它的元素中。但是, Button 组件已经有一个名为 styleprop,它将被覆盖。确保 HOC 可以通过重命名道具或合并道具来处理意外的名称冲突。

  1. 当使用多个组合 HOC 并将 props 传递给包含在其中的元素时,很难确定哪个 HOC 负责哪个 prop。这可能会阻碍应用程序的调试和扩展。
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)