React 中的高阶组件是什么?

Avatar of Kingsley Silas
Kingsley Silas

DigitalOcean 为您旅程的每个阶段提供云产品。 立即开始使用 200 美元的免费积分!

如果您在 React 生态系统中待了一段时间,您可能听说过 **高阶组件**。 让我们看一下简单的实现,同时尝试解释核心思想。 从这里,您应该了解它们的工作原理,甚至可以将它们投入使用。

为什么选择高阶组件?

在构建 React 应用程序时,您会遇到想要 *在多个组件中共享相同功能* 的情况。

例如:您需要管理应用程序中当前登录用户的状态。 而不是在所有需要该状态的组件中管理该状态,您可以创建一个高阶组件,将登录用户状态分离到一个容器组件中,然后将该状态传递给将使用它的组件。

从高阶组件接收状态的组件将充当 *展示* 组件。 状态被传递给它们,它们根据它有条件地渲染 UI。 它们不关心状态的管理。

让我们看另一个例子。 假设您的应用程序中有三个 JSON 文件。 这些文件包含将在三个不同组件中加载到应用程序中的不同数据。 您希望让用户能够*搜索*从这些文件加载的数据。 您可以在所有三个组件中实现搜索功能。 此重复在开始时可能不是问题,但随着应用程序的增长,更多组件需要此功能,持续重复将变得繁琐且容易出现问题。

更好的前进方向是创建一个高阶组件来处理搜索功能。 使用它,您可以将其他组件分别包装在您的高阶组件中。

高阶组件是如何工作的?

React 文档 表示高阶组件 **接收一个组件** 并 **返回一个组件**。

当您在架构上准备好将容器组件与展示组件分离时,高阶组件的使用非常方便。 展示组件通常是一个无状态函数组件,它接收道具并渲染 UI。 无状态函数组件是简单的 JavaScript 函数,没有状态。 以下是一个示例

import React from 'react'

const App = ({name}) => {
  return (
    <div>
      <h2>This is a functional component. Your name is {name}.</h2>
    </div>
  )
}

ReactDOM.render(<App name='Kingsley' />, document.getElementById("root"));

容器组件负责管理状态。 在这种情况下,容器是高阶组件。

在之前提到的搜索示例中,搜索组件将是管理搜索状态并包装需要搜索功能的展示组件的容器组件。 展示组件通常不知道状态或如何管理状态。

高阶组件示例

让我们从一个基本的示例开始。 以下是一个将用户名转换为大写并返回它的高阶组件

const hoc = (WrappedComponent) => (props) => {
  return (
    <div>
      <WrappedComponent {...props}>
        {props.children.toUpperCase()}
      </WrappedComponent>
    </div>
  )
}

此高阶组件接收一个 WrappedComponent 作为参数。 然后它返回一个带有一些传递给它的道具的*新*组件,并创建一个 React 元素。 我们在 props.children 上调用 .toUpperCase(),将传递的 props.children 转换为大写。

为了使用这个高阶组件,我们需要创建一个接收道具并渲染子元素的组件。

const Username = (props) => (
  <div>{props.children}</div>
)

接下来,我们用高阶组件包装 Username。 让我们将其存储在一个变量中

const UpperCaseUsername = hoc(Username)

在我们的 App 组件中,我们现在可以像这样使用它

const App = () => (
  <div>
    <UpperCaseUsername>Kingsley</UpperCaseUsername>
  </div>
);

UpperCaseUsername 组件仅仅是 Username UI 的呈现,而 Username 又从充当高阶组件的 WrappedComponent 中获取状态。

更实用的高阶组件

想象一下,我们想创建一个包含一个搜索表单的地理位置列表,用于过滤它们。 JSON 将位于平面文件中,并作为单独的组件加载。 让我们从加载数据开始。

我们的第一个组件将为用户加载地理位置。 我们将使用 .map() 来遍历该 JSON 文件中包含的数据。

import React from 'react'
// Where the data is located
import preload from './locations.json'
// Manages the data
import LocationCard from './LocationCard'

// Renders the presentation of the data
const Location = (props) => {
  return (
    <div>
      <div>
        <div>
          <h2>Preferred Locations</h2>
        </div>
      </div>
      <div>
        {preload.data
          .map(location => <LocationCard key={location.id} {...location} />)}
      </div>
    </div>
  )
}
export default Location

此组件将在 LocationCard 组件中渲染数据。 我将其移到另一个组件以保持清晰。 此组件是一个处理数据呈现的函数组件。 来自文件的 data(地理位置)通过 props 接收,每个地理位置将被传递给 LocationCard 组件。

现在我们需要一个第二个组件,最终也需要搜索功能。 它将与我们刚刚构建的第一个组件非常相似,但它将具有不同的名称并从不同的地方加载数据。

我们希望用户能够使用 input 字段搜索项目。 应用上显示的项目列表应由搜索状态决定。 此功能将在我们正在使用的两个组件之间共享。 由于高阶组件的概念,我们可以创建一个搜索容器组件,并将其包装在其他组件周围。

让我们将组件命名为 withSearch。 此组件将渲染用于我们搜索的 input 字段,并管理我们的 searchTerm 状态。 searchTerm 将作为道具传递给包装的组件,它将用于过滤提取的数据

import React, { Component } from 'react'

const withSearch = (WrappedComponent) => {
  return class extends Component {
    state = {
      searchTerm: ''
    }
    handleSearch = event => {
      this.setState({ searchTerm: event.target.value })
    }
 
    render() {
      return (
        <div>
          <div>
            <input onChange={this.handleSearch} value={this.state.searchTerm} type="text" placeholder="Search" />
          </div>
          <WrappedComponent searchTerm={this.state.searchTerm} />
        </div>
      )
    }
  }
 
}
export default withSearch

searchTerm 被赋予了一个空字符串的状态。 用户在搜索框中输入的值将被获取,并用于设置 searchTerm 的新状态。 接下来,我们将 searchTerm 传递给 WrappedComponent。 我们将在过滤数据时使用它。

为了使用高阶组件,我们需要对我们的展示组件进行一些更改。

import React, { Component } from 'react'
// Where the data is located
import preload from './locations.json'
// Searches the data
import withSearch from './withSearch
// Manages the data
import LocationCard from './LocationCard'

// Renders the presentation of the data
const Location = (props) => {
  const { searchTerm } = props
  
  return (
    <div>
      <div>
        <div>
          <h2>Preferred Locations</h2>
        </div>
      </div>
      <div>
        {preload.data
          // Filter locations by the inputted search term
          .filter(location => `${location.name} ${location.zone} ${location.region}`.toUpperCase().indexOf(searchTerm.toUpperCase()) >= 0)
          // Loop through the locations
          .map(location => <LocationCard key={location.id} {...location} />)}
      </div>
    </div>
  )
}
export default withSearch(Location)

我们在上面做的第一件事是导入高阶组件。 然后,我们添加了一个 filter 方法来根据用户在搜索输入中输入的内容过滤数据。 最后,我们需要用 withSearch 组件将其包装起来。

查看 CodePen 上 Kingsley Silas Chijioke 的笔 hoc Pen (@kinsomicrote)。

结论

高阶组件不必令人恐惧。 在理解了基础知识之后,您可以通过抽象化可以在不同组件之间共享的功能来将该概念投入使用。

更多资源