将高阶组件(HOC)重构为 React Hooks

2025-05-24

将高阶组件(HOC)重构为 React Hooks

随着 React 16.8 版本的发布(也被冠以“The One With Hooks”的称号),人们期待已久的 Hooks 模式也随之推出。这种模式让你无需使用类即可使用状态、生命周期以及(几乎)任何其他 React 功能。如果你已经使用 React 多年,这或许会让你感到如释重负,或许会让你感到震惊。对我来说,这感觉就像是解脱了,因为我一直以来都更喜欢使用函数组件而不是类组件。为了避免处理过多的类组件,我正在做的一些项目正在使用高阶组件(HOC)来复用类逻辑——这可能会变得相当复杂。在这篇文章中,我将把其中一个 HOC 转换为自定义 Hook,以展示这种“新”模式的强大功能。

附注:您可以根据自己的喜好使用类或 Hook,因为目前还没有关于类使用的任何重大变更。
当您阅读本文时,您可能已经尝试过任何 Hook,或者至少阅读过很多相关内容。如果您还没有尝试过, React 官方文档中的这篇概述是一个很好的起点。

高阶组件(HOC)

如前所述,HOC 是一种在 React 应用程序中复用(类)组件逻辑的模式。这样,您无需重复示例中基于状态更新的逻辑,例如数据获取或路由。React 文档将 HOC 描述为“接受组件并返回新组件的函数”,大致意味着用作 HOC 输入的组件将被增强并作为另一个组件返回。HOC 在 React 中被诸如react-router或 之类的包广泛使用react-redux。这些包中的 HOC 示例包括withRouterconnectHOC。前者允许您在传递给它的任何组件中访问路由属性,而后者则可以从输入组件连接到 Redux 状态。

创建 HOC 并不难,React 官网的文档对此withDataFetching进行了详细的解释。我将通过创建一个名为 的新 HOC 来演示。这将为传递给此 HOC 的任何组件添加使用状态和生命周期的基本数据获取功能。使用 Github API,将创建一个组件来渲染我的公共仓库列表。

  • 首先创建一个函数,该函数接受一个组件作为输入,并基于该组件返回一个不同的组件。该函数的作用仅仅是构造一个WithDataFetching返回输入组件的新类组件WrappedComponent
import React from "react";

const withDataFetching = props => WrappedComponent => {
  class WithDataFetching extends React.Component {

    render() {
      return (
        <WrappedComponent />
      );
    }
  }

  return WithDataFetching;
};

export default withDataFetching;
Enter fullscreen mode Exit fullscreen mode
  • 之后,您可以使用状态和生命周期将数据获取逻辑添加到此函数中。在状态中constructor()设置初始值,而在异步componentDidMount()生命周期中使用fetch()方法完成数据获取。
import React from "react";

const withDataFetching = props => WrappedComponent => {
  class WithDataFetching extends React.Component {
    constructor() {
      super();
      this.state = {
        results: [],
        loading: true,
        error: ""
      };
    }

    async fetchData() {
      try {
        const data = await fetch(props.dataSource);
        const json = await data.json();

        if (json) {
          this.setState({
            results: json,
            loading: false
          });
        }
      } catch (error) {
        this.setState({
          loading: false,
          error: error.message
        });
      }
    }

    async componentDidMount() {
      this.fetchData();
    }

    // ...
  }

  return WithDataFetching;
};

export default withDataFetching;
Enter fullscreen mode Exit fullscreen mode
  • render()在方法中,WrappedComponent将返回 ,并且状态值loadingresultserror应该作为 props 传递给它。这样,数据获取返回的结果将在输入组件上可用。
import React from "react";

const withDataFetching = props => WrappedComponent => {
  class WithDataFetching extends React.Component {
    // ...

    render() {
      const { results, loading, error } = this.state;

      return (
        <WrappedComponent
          results={results}
          loading={loading}
          error={error}
          {...this.props}
        />
      );
    }
  }

  return WithDataFetching;
};

export default withDataFetching;
Enter fullscreen mode Exit fullscreen mode
  • 最后,你可以设置 HOC 返回的组件的显示名称,否则,这个新组件在 React DevTools 等应用中很难追踪。这可以通过设置组件displayName的来实现WithDataFetching
import React from "react";

const withDataFetching = props => WrappedComponent => {
  class WithDataFetching extends React.Component {
    // ...

    render() {
      // ...
    }
  }

  WithDataFetching.displayName = `WithDataFetching(${WrappedComponent.name})`;

  return WithDataFetching;
};

export default withDataFetching;
Enter fullscreen mode Exit fullscreen mode

这创建了一个 HOC,可用于向传递给此函数的任何组件添加数据获取功能。如您所见,此 HOC 设置为柯里化函数,这意味着它将接受多个参数。因此,您不仅可以将组件作为参数传递,还可以将其他值作为第二个参数传递。对于withDataFetchingHOC,您还可以发送一个包含组件 props 的对象,其中 propsdataSource用作方法的 url 。您传递给此对象的任何其他 props 都将在返回的 urlfetch()上进行传播。WrappedComponent

  • Repositories在名为HOC组件的函数组件中,withDataFetching必须导入它。该文件默认导出的是 HOC 组件,它接受Repositories组件本身以及一个包含字段 的对象dataSource。该字段的值是 Github API 的 URL,用于根据用户名检索仓库。
import React from "react";
import withDataFetching from "./withDataFetching";

function Repositories() {

  return '';
}

export default withDataFetching({
  dataSource: "https://api.github.com/users/royderks/repos"
})(Repositories);
Enter fullscreen mode Exit fullscreen mode
  • 由于 HOC 为Repositories组件添加了数据获取功能,因此 props loadingresultserror会被传递给该组件。它们由 中的状态和生命周期值组成withDataFetching,可用于显示所有仓库的列表。当对 Github API 的请求尚未解析或发生错误时,将显示一条消息而不是仓库列表。
import React from "react";
import withDataFetching from "./withDataFetching";

function Repositories({ loading, results, error }) {
  if (loading || error) {
    return loading ? "Loading..." : error.message;
  }

  return (
    <ul>
      {results.map(({ id, html_url, full_name }) => (
        <li key={id}>
          <a href={html_url} target="_blank" rel="noopener noreferrer">
            {full_name}
          </a>
        </li>
      ))}
    </ul>
  );
}

export default withDataFetching({
  dataSource: "https://api.github.com/users/royderks/repos"
})(Repositories);
Enter fullscreen mode Exit fullscreen mode

通过这最后的更改,Repositories可以显示在 HOC 中完成的数据获取结果。这可以用于任何端点或组件,因为 HOC 使逻辑复用变得容易。

在下面的CodeSandbox中,您可以看到将Repositories组件传递给 HOC 的结果:

自定义钩子

在本文的引言中,我提到 Hooks 使得在类组件之外使用 React 特性(例如状态)成为可能。更正一下:Hooks 只能在函数组件中使用。此外,通过构建自定义 Hooks,你可以以几乎相同的方式重用之前 HOC 中的数据获取逻辑。但首先,让我们简单了解一下 Hooks,特别是useState()useEffect()Hook。

  • Hook让您可以处理任何函数组件的状态,useState()无需使用constructor()and/orthis.setState()方法。

  • Hook相当于生命周期方法。仅使用这个 Hook useEffect()你就可以监视特定(状态)变量的更新,或者根本不监视任何变量。componentDidMount()componentDidUpdate()

如果您还不熟悉这些 Hook,这可能听起来有些令人困惑,但幸运的是,您将使用这两个 Hook 来创建一个自定义useDataFetching()Hook。此 Hook 将具有与 HOC 相同的数据获取逻辑withDataFetching,并使用 方法调用 Github API fetch()。此 Hook 将返回与 HOC 相同的值,即loadingresultserror

  • useDataFetching首先,你需要创建一个接受参数的函数dataSource,该参数是稍后需要获取的 URL。react由于你想使用 React 特性,所以需要将这个自定义 Hook 作为依赖项,并从中导入你将要使用的两个 Hook。
import React, { useState, useEffect } from "react";

function useDataFetching(dataSource) {

  return {};
}

export default useDataFetching;
Enter fullscreen mode Exit fullscreen mode
  • Hook 应该返回值loadingresultserror;这些值必须添加到此 Hook 的状态中,然后返回。使用useState()Hook,您可以创建这些状态值,以及一个用于更新这些值的函数。但首先要创建状态值,并在函数末尾返回它们useDataFetching
import React, { useState, useEffect } from "react";

function useDataFetching(dataSource) {
  const [loading, setLoading] = useState(true);
  const [results, setResults] = useState([]);
  const [error, setError] = useState("");

  return {
    loading,
    results,
    error
  };
}

export default useDataFetching;
Enter fullscreen mode Exit fullscreen mode

返回值的初始值在调用useStateHook 时设置,可以使用 Hook 返回的数组的第二个值进行更新。第一个值是当前状态值,因此应在自定义 Hook 结束时返回。

  • 在 HOC中,withDataFetching有一个名为 的函数用于向 Github API 发送请求fetchData。此函数也必须添加到自定义 Hook 中。唯一的区别在于,状态值不是使用this.setState()方法来更新,而是通过调用 Hook 返回的更新函数来更新useState()。此fetchData函数必须放在useEffect()Hook 内部,这样您就可以控制何时调用此函数。
import React, { useState, useEffect } from "react";

function useDataFetching(dataSource) {
  const [loading, setLoading] = useState(true);
  const [results, setResults] = useState([]);
  const [error, setError] = useState("");

  useEffect(() => {
    async function fetchData() {
      try {
        const data = await fetch(dataSource);
        const json = await data.json();

        if (json) {
          setLoading(false);
          setResults(json);
        }
      } catch (error) {
        setLoading(false);
        setError(error.message);
      }

      setLoading(false);
    }

    fetchData();
  }, [dataSource]);

  return {
    error,
    loading,
    results
  };
}

export default useDataFetching;
Enter fullscreen mode Exit fullscreen mode

在上面的代码块中,fetchData当的值dataSource更新时,将调用该函数,因为该值被添加到 Hook 的依赖数组中useEffect()

现在,您可以从函数组件中调用自定义useDataFetching()Hook 来使用该组件中的数据获取值。与 HOC 不同,这些值不是作为 props 添加到组件中,而是由 Hook 返回的。

  • 在一个名为 的新函数组件中,RepositoriesHooks您需要导入useDataFetching()并解构 的值loadingresults以及error此 Hook 返回的结果。从 Github API 检索用户所有存储库的 URL 应作为参数添加。
import React from "react";
import useDataFetching from "./useDataFetching";

function RepositoriesHooks() {
  const { loading, results, error } = useDataFetching("https://api.github.com/users/royderks/repos");

  return '';
}

export default RepositoriesHooks;
Enter fullscreen mode Exit fullscreen mode
  • 要在列表中显示存储库,您可以复制组件的返回值,因为除了在此组件中添加和的Repositories值的方式之外,没有任何变化。loadingresultserror
import React from "react";
import useDataFetching from "./useDataFetching";

function RepositoriesHooks() {
  const { loading, results, error } = useDataFetching(
    "https://api.github.com/users/royderks/repos"
  );

  if (loading || error) {
    return loading ? "Loading..." : error.message;
  }

  return (
    <ul>
      {results.map(({ id, html_url, full_name }) => (
        <li key={id}>
          <a href={html_url} target="_blank" rel="noopener noreferrer">
            {full_name}
          </a>
        </li>
      ))}
    </ul>
  );
}

export default RepositoriesHooks;
Enter fullscreen mode Exit fullscreen mode

通过创建自定义useDataFetchingHook,您现在可以使用 React Hooks 在任何函数组件中使用数据获取,而无需创建 HOC。如果您想在CodeSandbox中查看受影响的更改,则需要注释掉该Repositories组件的导入src/index.js,并改为导入该RepositoriesHooks组件。

import React from "react";
import ReactDOM from "react-dom";

// import Repositories from "./Repositories";
import { default as Repositories } from "./RepositoriesHooks";

function App() {
  return (
    <div className="App">
      <h1>My Github repos</h1>
      <Repositories />
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Enter fullscreen mode Exit fullscreen mode

概括

新的 Hooks 模式使得在类组件之外使用 React 的状态、生命周期和其他功能成为可能。之前,您只能在类组件中使用这些功能,因此需要高阶组件 (HOC) 来复用您放入其中的任何逻辑。从 React 16.8 版本开始,您可以使用 Hook 从函数组件访问 React 功能,例如状态。通过创建自定义 Hook(例如useDataFetching()上面的 Hook),您可以在任何函数组件中复用示例中的状态逻辑。

希望这篇文章能帮助你决定是否应该将你的 HOC 转换为自定义 Hook!别忘了留下任何反馈,或者在Twitter上关注我,及时了解最新动态😄!

文章来源:https://dev.to/gethackteam/from-higher-order-components-hoc-to-react-hooks-2bm9
PREV
校园专家申请:2024 年 8 月!
NEXT
Babel 和 Webpack 到底是什么?2 分钟讲解完毕。Babel Webpack 总结一下: