Hooks 模式

React 16.8 引入了一个名为 Hooks 的新功能。
Hooks 使得使用 React 状态和生命周期方法成为可能,而无需使用 ES2015 类组件。

尽管 Hooks 不一定是一种设计模式,但 Hooks 在您的应用程序设计中扮演着非常重要的角色。
许多传统的设计模式可以被 Hooks 取代。


class 组件(X)

在 React 中引入 Hooks 之前,我们必须使用类组件来向组件添加状态和生命周期方法。
React 中的典型类组件可能如下所示:

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

class MyComponent extends React.Component {
  /* 添加状态和绑定自定义方法 */
  constructor() {
    super()
    this.state = { ... }

    this.customMethodOne = this.customMethodOne.bind(this)
    this.customMethodTwo = this.customMethodTwo.bind(this)
  }

  /* 生命周期方法 */
  componentDidMount() { ...}
  componentWillUnmount() { ... }

  /* 自定义方法 */
  customMethodOne() { ... }
  customMethodTwo() { ... }

  render() { return { ... }}
}

类组件可以在其构造函数中包含状态、生命周期方法
(例如 componentDidMount 和 componentWillUnmount 以根据组件的生命周期执行副作用)以及自定义方法以向类添加额外逻辑。

class 组件的缺点

尽管引入 React Hooks 后我们仍然可以使用类组件,但使用类组件可能会有一些缺点!
让我们看一下使用类组件时最常见的一些问题。

了解 ES2015 类

由于在 React Hooks 之前,类组件是唯一可以处理状态和生命周期方法的组件,因此我们经常不得不将功能组件重构为类组件,以添加额外的功能。

在此示例中,有一个用作按钮的简单 div。

function Button() {
  return <div className="btn">disabled</div>;
}

我们希望在用户单击按钮时将其更改为启用,而不是始终显示禁用,并在发生这种情况时为按钮添加一些额外的 CSS 样式。

为了做到这一点,我们需要向组件添加状态,以便知道状态是启用还是禁用。
这意味着我们必须完全重构功能组件,并使其成为跟踪按钮状态的类组件。

export default class Button extends React.Component {
  constructor() {
    super();
    this.state = { enabled: false };
  }

  render() {
    const { enabled } = this.state;
    const btnText = enabled ? "enabled" : "disabled";

    return (
      <div
        className={`btn enabled-${enabled}`}
        onClick={() => this.setState({ enabled: !enabled })}
      >
        {btnText}
      </div>
    );
  }
}

最后,按钮按我们想要的方式工作!

在这个例子中,组件非常小,重构并不是什么大事。
然而,现实生活中的组件可能包含更多的代码行,这使得重构组件变得更加困难。

除了必须确保在重构组件时不会意外更改任何行为,
您还需要了解 ES2015 类的工作原理。 为什么我们必须绑定自定义方法? 构造函数是做什么的? this 关键字从何而来?
很难知道如何正确重构组件而不意外更改数据流。

重构

在多个组件之间共享代码的常用方法是使用高阶组件或渲染道具模式。
尽管这两种模式都是有效的并且是一种很好的做法,但稍后添加这些模式需要您重新构建应用程序。

除了必须重构您的应用程序(组件越大越棘手),为了在更深的嵌套组件之间共享代码而拥有许多包装组件可能会导致最好将其称为包装器地狱。
打开开发工具并看到类似于以下内容的结构并不少见:

<WrapperOne>
  <WrapperTwo>
    <WrapperThree>
      <WrapperFour>
        <WrapperFive>
          <Component>
            <h1>Finally in the component!</h1>
          </Component>
        </WrapperFive>
      </WrapperFour>
    </WrapperThree>
  </WrapperTwo>
</WrapperOne>

包装器地狱会使您难以理解数据如何流经您的应用程序,从而更难弄清楚为什么会发生意外行为。

复杂

随着我们向类组件添加更多逻辑,组件的大小会快速增加。
该组件内的逻辑可能会变得混乱和非结构化,这会使开发人员难以理解类组件中使用某些逻辑的位置。
这会使调试和优化性能变得更加困难。

生命周期方法也需要大量的代码重复。
我们来看一个例子,它使用了一个 Counter 组件和一个 Width 组件。

import React from "react";
import "./styles.css";

import { Count } from "./Count";
import { Width } from "./Width";

export default class Counter extends React.Component {
  constructor() {
    super();
    this.state = {
      count: 0,
      width: 0
    };
  }

  componentDidMount() {
    this.handleResize();
    window.addEventListener("resize", this.handleResize);
  }

  componentWillUnmount() {
    window.removeEventListener("resize", this.handleResize);
  }

  increment = () => {
    this.setState(({ count }) => ({ count: count + 1 }));
  };

  decrement = () => {
    this.setState(({ count }) => ({ count: count - 1 }));
  };

  handleResize = () => {
    this.setState({ width: window.innerWidth });
  };

  render() {
    return (
      <div className="App">
        <Count
          count={this.state.count}
          increment={this.increment}
          decrement={this.decrement}
        />
        <div id="divider" />
        <Width width={this.state.width} />
      </div>
    );
  }
}

App 组件的结构方式可以如下所示:

虽然这是一个很小的组件,但是组件内部的逻辑已经很混乱了。
某些部分特定于计数器逻辑,而其他部分特定于宽度逻辑。 随着组件的增长,在组件内构建逻辑、在组件内找到相关逻辑会变得越来越困难。

除了混乱的逻辑,我们还在生命周期方法中复制了一些逻辑。
在 componentDidMount 和 componentWillUnmount 中,我们根据窗口的调整大小事件自定义应用程序的行为。


Hooks

很明显,类组件在 React 中并不总是一个很好的特性。
为了解决 React 开发者在使用类组件时可能遇到的常见问题,React 引入了 React Hooks。
React Hooks 是可用于管理组件状态和生命周期方法的函数。

React Hooks 可以:

  • 向功能组件添加状态
  • 管理组件的生命周期,而无需使用诸如 componentDidMount 和 componentWillUnmount 之类的生命周期方法
  • 在整个应用程序的多个组件之间重用相同的有状态逻辑

首先,让我们看一下如何使用 React Hooks 向功能组件添加状态。

state

React 提供了一个钩子来管理功能组件中的状态,称为 useState

让我们看看如何使用 useState 钩子将类组件重组为功能组件。
有一个名为 Input 的类组件,它只是渲染一个输入字段。
每当用户在输入字段中输入任何内容时,状态中输入的值就会更新。

class Input extends React.Component {
  constructor() {
    super();
    this.state = { input: "" };

    this.handleInput = this.handleInput.bind(this);
  }

  handleInput(e) {
    this.setState({ input: e.target.value });
  }

  render() {
    <input onChange={handleInput} value={this.state.input} />;
  }
}

为了使用 useState 钩子,我们需要访问 React 为我们提供的 useState 方法。
useState 方法需要一个参数:这是状态的初始值,在本例中为空字符串。

我们可以从 useState 方法中解构两个值:

  • 状态的当前值
  • 更新状态的方法
const [value, setValue] = React.useState(initialValue);

第一个值可以与类组件的 this.state.[value] 进行比较。
第二个值可以与类组件的 this.setState 方法进行比较。

由于我们正在处理输入的值,让我们调用状态输入的当前值,以及更新状态 setInput 的方法。 初始值应该是一个空字符串。

const [input, setInput] = React.useState("");

现在可以将 Input 类组件重构为有状态的功能组件。

function Input() {
  const [input, setInput] = React.useState("");
  console.log(input)

  return <input onChange={(e) => setInput(e.target.value)} value={input} />;
}

输入字段的值等于输入状态的当前值,就像在类组件示例中一样。
当用户在输入字段中输入时,输入状态的值会使用 setInput 方法相应更新。

Effect Hook

我们已经看到可以使用 useState 组件来处理功能组件内的状态,但是类组件的另一个好处是可以向组件添加生命周期方法。

使用 useEffect 钩子,我们可以“钩入”组件生命周期。
useEffect 钩子有效地结合了 componentDidMountcomponentDidUpdatecomponentWillUnmount 生命周期方法。

componentDidMount() { ... }
useEffect(() => { ... }, [])

componentWillUnmount() { ... }
useEffect(() => { return () => { ... } })

componentDidUpdate() { ... }
useEffect(() => { ... })

让我们使用在 state 部分中使用的输入示例。 每当用户在输入字段中输入任何内容时,我们还希望将该值记录到控制台。

我们需要使用“监听”输入值的 useEffect 钩子。
我们可以通过将输入添加到 useEffect 钩子的依赖项数组来实现。 依赖数组是 useEffect 钩子接收的第二个参数。

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

export default function Input() {
  const [input, setInput] = useState("");

  useEffect(() => {
    console.log(`The user typed ${input}`);
  }, [input]);

  return (
    <input
      onChange={e => setInput(e.target.value)}
      value={input}
      placeholder="Type something..."
    />
  );
}

每当用户键入值时,输入的值现在都会记录到控制台。

自定义 Hooks

除了 React 提供的内置钩子
useStateuseEffectuseReduceruseRefuseContextuseMemouseImperativeHandleuseLayoutEffectuseDebugValueuseCallback),

我们还可以轻松创建自己的自定义钩子。

你可能已经注意到,所有的钩子都是从use开头的。 为了让 React 检查它是否违反了 Hooks 的规则,开头use钩子是很重要的。

Hooks 的另一个巨大优势是社区可以构建和共享 hooks。
我们有时候没必要自己编写钩子!
自定义钩子已经由其他人构建,如果安装它,就可以在我们的应用程序中使用它!

这里有一些网站列出了社区构建的所有钩子,并准备在您的应用程序中使用。


附加 Hooks 指南

添加 Hooks

与其他组件一样,当您想将 Hooks 添加到您编写的代码中时,会使用一些特殊的函数。
以下是一些常见 Hook 函数的简要概述:

1. useState

useState Hook 使开发人员能够更新和操作函数组件内的状态,而无需将其转换为类组件。
这个 Hook 的一个优点是它很简单,不需要像其他 React Hook 那样复杂。

2. useEffect

useEffect Hook 用于在函数组件中的主要生命周期事件期间运行代码。

函数组件的主体不允许突变、订阅、计时器、日志记录和其他副作用。 如果允许,则可能会导致 UI 中出现令人困惑的错误和不一致。

useEffect 钩子可以防止所有这些“副作用”,并允许 UI 平稳运行。
它是 componentDidMount 、 componentDidUpdate 和 componentWillUnmount 的组合。

3. useContext

useContext Hook 接受一个上下文对象,它是从 React.createcontext 返回的值,并返回该上下文的当前上下文值。
useContext Hook 还与 React Context API 一起使用,以便在整个应用程序中共享数据,而无需通过各个级别向下传递您的应用程序道具。

应该注意的是,传递给 useContext 钩子的参数必须是上下文对象本身,并且任何调用 useContext 的组件总是在上下文值更改时重新渲染。

4. useReducer

useReducer Hook 提供了 setState 的替代方案,当您有涉及多个子值的复杂状态逻辑或下一个状态依赖于前一个状态时,尤其适合使用它。

它接受一个 reducer 函数和一个初始状态输入,并通过数组解构的方式返回当前状态和一个 dispatch 函数作为输出。

useReducer 还优化了触发深度更新的组件的性能。

使用 Hooks 的优缺点

以下是使用 Hook 的一些好处:

更少的代码行 Hooks 允许您按关注点和功能而不是按生命周期对代码进行分组。
这使得代码不仅更清晰简洁,而且更短。
下面是一个使用 React 的可搜索产品数据表的简单无状态组件的比较,以及使用 useState 关键字后它在 Hooks 中的外观。

 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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

//class无状态组件
class TweetSearchResults extends React.Component {
    constructor(props) {
      super(props);
      this.state = {
        filterText: '',
        inThisLocation: false
      };

      this.handleFilterTextChange = this.handleFilterTextChange.bind(this);
      this.handleInThisLocationChange = this.handleInThisLocationChange.bind(this);
    }

    handleFilterTextChange(filterText) {
      this.setState({
        filterText: filterText
      });
    }

    handleInThisLocationChange(inThisLocation) {
      this.setState({
        inThisLocation: inThisLocation
      })
    }

    render() {
      return (
        <div>
          <SearchBar
            filterText={this.state.filterText}
            inThisLocation={this.state.inThisLocation}
            onFilterTextChange={this.handleFilterTextChange}
            onInThisLocationChange={this.handleInThisLocationChange}
          />
          <TweetList
            tweets={this.props.tweets}
            filterText={this.state.filterText}
            inThisLocation={this.state.inThisLocation}
          />
        </div>
      );
    }
  }
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21

//使用 Hooks 相同的组件
const TweetSearchResults = ({tweets}) => {
  const [filterText, setFilterText] = useState('');
  const [inThisLocation, setInThisLocation] = useState(false);
  return (
    <div>
      <SearchBar
        filterText={filterText}
        inThisLocation={inThisLocation}
        setFilterText={setFilterText}
        setInThisLocation={setInThisLocation}
      />
      <TweetList
        tweets={tweets}
        filterText={filterText}
        inThisLocation={inThisLocation}
      />
    </div>
  );
}

简化复杂的组件

JavaScript 类可能难以管理,难以与热重载一起使用,并且可能无法缩小。

React Hooks 解决了这些问题并确保函数式编程变得简单。 通过 Hooks 的实现,我们不需要类组件。

重用有状态逻辑

JavaScript 中的类鼓励多层次的继承,这会迅速增加整体复杂性和出错的可能性。

但是,Hooks 允许您在不编写类的情况下使用状态和其他 React 功能。
使用 React,您可以随时重用有状态逻辑,而无需一遍又一遍地重写代码。 这减少了出错的机会,并允许使用普通函数进行组合。

共享非视觉逻辑

在 Hooks 实现之前,React 无法提取和共享非可视化逻辑。
这最终导致了更多的复杂性,例如 HOC 模式和 render 道具,只是为了解决一个常见问题。

但是,Hooks 的引入解决了这个问题,因为它允许将有状态的逻辑提取到一个简单的 JavaScript 函数中。

当然,Hooks 有一些潜在的缺点值得牢记:

  • 必须尊重它的规则,没有 linter 插件,很难知道哪个规则被破坏了。
  • 需要相当长的时间练习才能正确使用(Exp:useEffect)。
  • 注意错误的使用(Exp:useCallback,useMemo)。

Hooks vs 类

使用 React Hooks 可以更清晰地将我们组件的逻辑分成几个更小的部分。
重用相同的有状态逻辑变得更加容易,如果我们想让组件有状态,我们不再需要将功能组件重写为类组件。
不再需要对 ES2015 类有很好的了解,并且具有可重用的有状态逻辑增加了组件的可测试性、灵活性和可读性。

当 Hooks 被引入 React 时,它产生了一个新问题:我们如何知道什么时候使用带有 Hooks 和类组件的函数组件?

在 Hooks 的帮助下,即使在函数组件中也可以获取状态和部分生命周期 Hooks

Hooks 还允许您在不编写类的情况下使用本地状态和其他 React 功能。

以下是 Hooks 和 Classes 之间的一些差异,以帮助您做出决定:

React Hooks class
它有助于避免多个层次结构并使代码更清晰 一般来说,当你使用 HOC 或 renderProps 时,当你尝试在 DevTools 中看到它时,你必须用多个层次结构重构你的 App
它提供了 React 组件之间的一致性。 由于需要了解绑定和调用函数的上下文,类会混淆人类和机器。

知识点

  • useState
  • useEffect
  • useContext
  • useReducer