HOC 模式(X)

这是历史遗留问题,所以不建议使用(现在有了Hooks),在这里只是让我们了解这段历史

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

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

高阶组件 (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 StyedText = withStyles(Text)

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

让我们看一下之前在 Container/Presentational 模式中使用的同一个 DogImages 示例!该应用程序仅渲染从 API 获取的狗图像列表。

HOC 模式

让我们稍微改善一下用户体验。 当我们获取数据时,我们希望向用户显示“正在加载…”屏幕。
我们可以使用高阶组件为我们添加此逻辑,而不是直接向 DogImages 组件添加数据。

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

让我们创建我们想要使用的 withLoader HOC h 的最低版本!

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 传递。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15

//DogImages.js
import React from "react";
import withLoader from "./withLoader";

function DogImages(props) {
  return props.data.message.map((dog, index) => (
    <img src={dog} alt="Dog" key={index} />
  ));
}

export default withLoader(
  DogImages,
  "https://dog.ceo/api/breed/labrador/images/random/6"
);
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

//withLoader.js
import React, { useEffect, useState } from "react";

export default function withLoader(Element, url) {
  return (props) => {
    const [data, setData] = useState(null);

    useEffect(() => {
      async function getData() {
        const res = await fetch(url);
        const data = await res.json();
        setData(data);
      }

      getData();
    }, []);

    if (!data) {
      return <div>Loading...</div>;
    }

    return <Element {...props} data={data} />;
  };
}

Perfect! 完美的! 我们刚刚创建了一个可以接收任何组件和 url 的 HOC。

  1. 在 useEffect 钩子中,withLoader HOC 从我们作为 url 值传递的 API 端点获取数据。 虽然数据尚未返回,但我们返回包含 Loading… 文本的元素。

  2. 获取数据后,我们将 data 设置为等于已获取的数据。 由于数据不再为空,我们可以显示我们传递给 HOC 的元素!

那么,我们如何将这种行为添加到我们的应用程序中,以便它实际上会在 DogImages 列表上显示 Loading… 指示器?

在 DogImages.js 中,我们不再只想导出普通的 DogImages 组件。
相反,我们希望围绕 DogImages 组件导出“包装的” withLoading HOC。

export default withLoading(DogImages);

withLoading HOC 还希望 url 知道从哪个端点获取数据。 在这种情况下,我们要添加 Dog API 端点。

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

由于 withLoader HOC 返回了带有额外数据道具的元素,在本例中为 DogImages,我们可以访问 DogImages 组件中的数据道具。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15

//DogImages.js
import React from "react";
import withLoader from "./withLoader";

function DogImages(props) {
  return props.data.message.map((dog, index) => (
    <img src={dog} alt="Dog" key={index} />
  ));
}

export default withLoader(
  DogImages,
  "https://dog.ceo/api/breed/labrador/images/random/6"
);
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

//withLoader.js
import React, { useEffect, useState } from "react";

export default function withLoader(Element, url) {
  return (props) => {
    const [data, setData] = useState(null);

    useEffect(() => {
      async function getData() {
        const res = await fetch(url);
        const data = await res.json();
        setData(data);
      }

      getData();
    }, []);

    if (!data) {
      return <div>Loading...</div>;
    }

    return <Element {...props} data={data} />;
  };
}

Perfect! 我们现在在获取数据时看到一个 Loading… 屏幕。

高阶组件模式允许我们为多个组件提供相同的逻辑,同时将所有逻辑保存在一个地方。

withLoader HOC 不关心它接收到的组件或 url:只要它是一个有效的组件和一个有效的 API 端点,它就会简单地将数据从该 API 端点传递给我们传递的组件。


组合

我们还可以组合多个高阶组件。

假设还想添加显示悬停的功能! 当用户将鼠标悬停在 DogImages 列表上时的文本框。

需要创建一个 HOC,为传递的元素提供悬停道具。
基于该道具,可以根据用户是否将鼠标悬停在 DogImages 列表上,有条件地呈现文本框。

现在可以将 withHover HOC 包裹在 withLoader HOC 周围。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22

//DogImages.js
import React from "react";
import withLoader from "./withLoader";
import withHover from "./withHover";

function DogImages(props) {
  return (
    <div {...props}>
      {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 withHover(
  withLoader(DogImages, "https://dog.ceo/api/breed/labrador/images/random/6")
);
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18

//withHover.js
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)}
      />
    );
  };
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21

//withLoader.js
import React, { useEffect, useState } from "react";

export default function withLoader(Element, url) {
  return props => {
    const [data, setData] = useState(null);

    useEffect(() => {
      fetch(url)
        .then(res => res.json())
        .then(data => setData(data));
    }, []);

    if (!data) {
      return <div>Loading...</div>;
    }

    return <Element {...props} data={data} />;
  };
}

DogImages 元素现在包含从 withHover 和 withLoader 传递的所有道具。
现在可以有条件地渲染悬停! 文本框,基于悬停道具的值是真还是假。

一个著名的用于组成 HOC 的库是 recompose
由于 HOC 在很大程度上可以被 React Hooks 替代,因此不再维护 recompose 库,因此本文不会涉及


优点

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


缺点

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

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)

当使用多个组合的 HOC 将 props 传递给包裹在其中的元素时,可能很难确定哪个 HOC 负责哪个 prop。 这可能会阻碍调试和轻松扩展应用程序。