本文翻译自 https://reactjs.org/docs/hooks-intro.html  并加入一些自己的观点。

为什么会有Hooks?

在组件之间复用有状态逻辑很困难

React并没有为组件提供一种可重复使用行为的方法(例如关联到一个状态),如果你已经使用React一段时间,你可能熟悉利用render props(渲染属性)和higher-order components(高阶组件)来试图解决这个问题。但是这种模式你在使用的时候得重构你的组件,并且代码看起来很笨重且不易维护。如果你在React DevTools看一下典型的React应用程序,您可能会发现由providers, consumers, higher-order components, render props, 和其他抽象层(other abstractions.)包裹组件的“包装器地狱”,虽然我们可以在DevTools中过滤它们,但这引发了一个更深层次的问题:React需要一个更好的方式来共享有状态的逻辑。

使用Hooks,您可以从组件中提取有状态逻辑,以便可以独立测试并复用。Hooks允许您在不更改组件层次结构的情况下复用有状态的逻辑。 这样可以轻松地在许多组件之间共享Hooks。

复杂的组件变得难以理解

我们经常得维护那些刚开始非常简单的组件,但是因为状态逻辑和一些副作用变得难以维护。每个生命周期都通常包含一些不相关逻辑。例如,组件可能会在componentDidMountcomponentDidUpdate中执行一些数据获取,但是,相同的componentDidMount方法可能还包含设置一些event listeners等无关逻辑,并在componentWillUnmount中执行清理。一些相关的代码会被分割,但是一些无关的代码最后被写入到一个的方法中,这非常容易造成错误和不一致。

在许多情况下,很难将这些组件分解为较小的组件,因为状态逻辑遍布到处都是。测试它们也很困难。 这是许多人更喜欢将React与单独的状态管理库相结合的原因之一。但是,这通常会引入太多的抽象,要求您在不同的文件之间跳转,并使复用组件变得更加困难。

为了解决这个问题,Hooks允许你根据相关的内容(例如设置订阅或获取数据)将一个组件拆分为较小的函数,而不是基于生命周期方法强制拆分。 您还可以选择使用reducer管理组件的本地state(状态),以使其更具可预测性。

Class 令人和机器都容易混淆

除了使代码复用和代码组织更加困难之外,我们发现class可能是学习React的一大障碍。你必须了解this在JavaScript中是如何工作的,这与它在大多数语言中的工作方式有很大的不同。你必须记住绑定事件处理程序。 没有不稳定的语法提议,代码非常冗长。但是人们可以很好地理解props(属性),state(状态)和自上而下的数据流,但仍然很难与class(类)斗争。 React中的函数和类组件之间的区别以及何时使用每个组件,即使在经验丰富的React开发人员之间也存在分歧。

此外,React已经推出了大约五年,我们希望确保它在未来五年内保持相关性。正如Svelte,Angular,Glimmer和其他人所表明的那样,提前编译组件具有很大的未来潜力。特别是如果它不限于模板。 最近,我们一直在尝试使用Prepack进行组件折叠,并且我们已经看到了有希望的早期结果。但是,我们发现类组件可能会鼓励无意识的模式,使这些优化回归到较慢的路径。类也为今天的工具提出了问题。 例如,类不会很好地缩小,并且它们使得热重新加载片状和不可靠。 我们希望提供一种API,使代码更有可能保持在可优化的路径上。

为了解决这些问题,Hooks允许您在没有类的情况下使用更多React的功能。从概念上讲,React组件一直更接近function(函数)。 Hooks拥抱function(函数),但不会和react的宗旨相违背。钩子提供了对命令式逃生舱口的访问,并且不需要您学习复杂的函数或反应式编程技术。

React 升级策略

采用逐步策略

我们知道React开发人员专注于发布产品,没有时间研究正在发布的每个新API。 Hooks是非常新的,在考虑学习或采用它们之前等待更多示例和教程可能会更好。我们也理解为React添加新原语的标准非常高。 对于好奇的读者,我们已经准备了一个详细的RFC,其中包含更多细节的动机,并提供有关特定设计决策和相关现有技术的额外视角。

至关重要的是,Hooks与现有代码并行工作,因此您可以逐步采用它们。 我们正在分享这个实验性的API,以便从社区中那些有兴趣塑造React未来的人那里获得早期反馈 - 我们将在公开场合迭代Hooks。

最后,没有急于迁移到Hooks。 我们建议避免任何“重大重构”,特别是对于现有的复杂类组件。 开始“在Hooks中思考”需要一点精神上的转变。 根据我们的经验,最好先在新的和非关键组件中练习使用Hooks,并确保团队中的每个人都对它们感到满意。

我们打算让Hooks涵盖所有现有的类用例,但我们将在可预见的未来继续支持类组件。 在Facebook,我们有数万个用类编写的组件,我们绝对没有计划重构它们。 相反,我们开始在新代码中同时使用Hooks与class。

没有重大变化

  1. 可选性引入      你可以尝试使用hooks 在你的组件中,并且不必重写一部分已经存在的代码。如果你不想使用hooks,你可以不学习hooks。
  2. 100%向后兼容 hooks     没有包含破坏性的变化
  3. 现在使用hooks        现在正在alpha版本(10天前已经发布了17.6.0稳定版本)

提示:

没有计划从react移除class

hooks不会改变你对react已知的概念

简介Hooks

有时,我们希望在组件之间重用一些有状态逻辑。 传统上,这个问题有两个流行的解决方案: higher-order componentsrender props. 。 自定义挂钩允许您执行此操作,但无需向树中添加更多组件。

什么是Hooks?

我们新的例子是用从React导入useState hook

import { useState } from 'react';

function Example() {
  // ...
}

什么是hooks?

Hooks是一种特殊函数,可让你“hook into(勾住)”React 状态和生命周期的功能。 例如,useState是一个Hooks,允许你将React状态添加到函数组件。

什么时候使用Hooks?

如果你编写一个函数组件并意识到你需要添加一些状态,以前你需要通过class 方式来实现。现在,您可以在现有功能组件中使用Hooks。

hooks 规则

Hooks 是 JavaScript 函数,但强加了两个额外的规则:

1、只能在顶层调用Hooks,不要在循环,条件或嵌套函数中调用Hooks。

2、 仅从React函数组件调用Hooks。 不要从常规JavaScript函数中调用Hooks。

Hooks 介绍

下面我们开始介绍两种hooks:State Hook、Effect Hook

State Hook

import { useState } from 'react';

function Example() {
  // Declare a new state variable, which we'll call "count"
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

等价于

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

状态以{count:0}开始,当用户通过调用this.setState()单击按钮时,我们会增加state.count。

Hooks and Function Components(钩子和函数组件)

提醒一下,React中的函数组件如下所示:

const Example = (props) => {
  // You can use Hooks here!
  return <div />;
}

或者这样

function Example(props) {
  // You can use Hooks here!
  return <div />;
}

您可能以前将这些称为“stateless components(无状态组件)”。 我们现在介绍从这些中使用React状态的能力,所以我们更喜欢名称“function components(函数组件)”。

Hooks 不能再class 方式编写,但是你可以用函数组件的方式编写。

读取state(状态)

当我们想要在类中显示当前计数时,我们读取this.state.count:

<p>You clicked {this.state.count} times</p>

在函数中,我们可以直接使用count:

<p>You clicked {count} times</p>

更新state(状态)

在class中,我们需要调用this.setState()来更新计数状态:

<button onClick={() => this.setState({ count: this.state.count + 1 })}>
    Click me
</button>

在函数中,我们已经将setCount和count作为变量,因此我们不需要这样:

<button onClick={() => setCount(count + 1)}>
    Click me
</button>

Effect Hook

介绍

Effect Hook可让您在功能组件中执行Side Effect(副作用):

import { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  // Similar to componentDidMount and componentDidUpdate:
  useEffect(() => {
    // Update the document title using the browser API
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}
React 组件中有两种常见的 side effects :不需要清理的和需要清理的。让我们更详细地看看这个区别。

不需要清理的 side effects

有时,我们希望在React更新DOM之后运行一些额外的代码。 网络请求,手动DOM突变和日志记录是不需要清理的效果的常见示例。 我们这样说是因为我们可以运行它们并立即忘记它们。 让我们比较一下class和hooks如何让我们表达这样的副作用。

比较class 和 hooks

使用class的例子:

在 React 类组件中,render 方法不应该引起 side effects 。 现在还为时过早——我们通常希望在 React 更新 DOM 之后执行我们的 side effects 。

这就是为什么在 React 类中,我们要将 side effects 放入 componentDidMountcomponentDidUpdate 中。 回到我们的示例,这里是一个 React 计数器类组件,它在 React 对 DOM 进行更改后立即更新文档标题:

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
  }

  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

请注意 我们在类中复制了这两个生命周期方法之间的代码

这是因为在许多情况下,我们希望执行相同的 side effect ,无论组件是刚mounted

还是updated。 从概念上讲,我们希望它发生在每次渲染之后,但是 React 类组件没有这样的方法。我们可以提取一个单独的方法,但是我们仍然需要在两个地方调用它。

现在让我们看看我们如何使用 useEffect Hook做同样的事情。

使用hooks例子:

import { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

useEffect做了什么?通过使用此Hook,您告诉React您的组件需要在渲染后执行某些操作,React将记住您传递的函数(我们将其称为“Effect”),并在执行DOM更新后稍后调用它。 在这个效果中,我们设置文档标题,但我们也可以执行数据提取或调用其他命令式API。

为什么在组件内调用useEffect?在组件内放置useEffect让我们可以直接从效果中访问计数状态变量(或任何props)。我们不需要特殊的API来读取它 ,它已经在函数范围内了。 Hooks支持JavaScript闭包,在JavaScript已经提供解决方案的情况下,避免引入特定于React的API。

每次渲染后useEffect都会运行吗?是的! 默认情况下,它在第一次渲染之后和每次更新之后运行。您可能会发现更容易认为effects发生在“渲染之后”,而不是“挂载”和“更新”。React保证DOM在运行效果时已更新。

详细说明

现在我们对 effects 有了一定的了解,我们来解释以下这几行代码的意义:

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

我们声明了 count state(状态)变量,然后告诉 React 我们需要使用 effect 。我们将一个函数传递给useEffect Hook,这个函数就 我们的 effect 。在我们的 effect 中,我们使用 document.title 浏览器 API 设置文档标题。我们可以读取 effect 中的最新count,因为它在我们的函数作用域内。当 React 渲染我们的组件时,它会记住我们使用的 effect ,然后在更新 DOM 后运行我们的effect 。每次渲染后都会发生,包括第一次渲染。

有经验的JavaScript开发人员可能会注意到,传递给 useEffect 的函数在每次渲染时都会有所不同。这是故意的。实际上,这就是我们从 effect 内部直接读取count而不用担心它没有更新的原因。我们每一次的重新渲染时,都会有新的 effect 来替换前者。在某种程度上说, effect 更像是渲染结果的一部分,每个effect都属于特定的渲染。

需要清理的 side effects

之前,我们研究了如何表达不需要任何清理的副作用。 但是,有些Effects会起作用。 例如,我们可能希望设置对某些外部数据源的订阅。 在这种情况下,清理是非常重要的,这样我们就不会引入内存泄漏! 让我们比较一下我们如何使用类和Hooks来实现它。

使用class(类)的例子:

在React类中,您通常会在componentDidMount中设置订阅,并在componentWillUnmount中清除它。 例如,假设我们有一个ChatAPI模块,可以让我们订阅朋友的在线状态。 以下是我们如何使用类订阅和显示该状态:

class FriendStatus extends React.Component {
  constructor(props) {
    super(props);
    this.state = { isOnline: null };
    this.handleStatusChange = this.handleStatusChange.bind(this);
  }

  componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  handleStatusChange(status) {
    this.setState({
      isOnline: status.isOnline
    });
  }

  render() {
    if (this.state.isOnline === null) {
      return 'Loading...';
    }
    return this.state.isOnline ? 'Online' : 'Offline';
  }
}

请注意componentDidMountcomponentWillUnmount如何相互作用的。 生命周期方法迫使我们拆分这个逻辑,即使它们中的概念代码都与相同的effect有关。

使用hooks的例子:

让我们看看我们如何用 Hooks 编写这个组件。

您可能认为我们需要单独的效果来执行清理。 但是添加和删除订阅的代码是如此紧密相关,以至于useEffect旨在将它保持在一起。 如果你的效果返回一个函数,React将在清理时运行它:

import { useState, useEffect } from 'react';

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);

  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    // Specify how to clean up after this effect:
    return function cleanup() {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

为什么在我们的effect方式中返回了一个函数?这是效果的可选清理机制。每个effect在执行之后都可以返回一个清理的函数。 这使我们可以保持添加和删除彼此接近的订阅的逻辑。 它们是同样效果的一部分!

React什么时候清理效果?当组件卸载时React执行清理。但是,正如我们之前所了解的那样,effect会针对每个渲染运行而不仅仅是一次,这就是React在下次运行effect之前还清除前一渲染效果的原因。

使用Effect的提示

  1. 提示:使用多个effects分离关注点

我们在Hooks的Motivation中概述的问题之一是类生命周期方法通常包含不相关的逻辑,但是相关的逻辑被分解成几种方法。这是一个组合了前面示例中的计数器和朋友状态指示器逻辑的组件:

class FriendStatusWithCounter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0, isOnline: null };
    this.handleStatusChange = this.handleStatusChange.bind(this);
  }

  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  handleStatusChange(status) {
    this.setState({
      isOnline: status.isOnline
    });
  }
  // ...

请注意设置document.title的逻辑如何在componentDidMountcomponentDidUpdate之间拆分。 订阅逻辑也在componentDidMountcomponentWillUnmount之间传播。 componentDidMount包含两个任务的代码。

那么,Hooks如何解决这个问题呢? 就像you can use the State Hook more than once,您也可以使用多种effects。 这让我们将不相关的逻辑分成不同的效果

function FriendStatusWithCounter(props) {
  const [count, setCount] = useState(0);
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }
  // ...
}

Hooks允许我们根据它正在做的事情而不是生命周期方法名称来拆分代码。 React将按照指定的顺序应用组件使用的每个效果。

2.说明:为什么effects在每个更新上运行?

如果你习惯了class,你可能想知道为什么每次重新渲染后效果清理阶段都会发生,而不是在卸载过程中只发生一次。 让我们看一个实际的例子,看看为什么这个设计可以帮助我们创建更少bug的组件。

在本页前面,我们介绍了一个示例FriendStatus组件,该组件显示friend是否在线。 我们的class从this.props读取friend.id,在组件挂载后订阅friend状态,并在卸载期间取消订阅:

componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
        this.props.friend.id,
        this.handleStatusChange
    );
}

componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
        this.props.friend.id,
        this.handleStatusChange
    );
}

但是如果friend prop 在渲染过程中发生了变化,会发生什么? 我们的组件将会显示一个错误id的在线状态。 因为我们的组件dom在数据更新后并没有及时的更新。这种情况下,还会因为取消订阅时候使用错误的friend ID(因为friend ID发生了变化,didMount与unmount的id 并不一致),导致内存泄漏或崩溃。

在类组件中,我们需要添加 componentDidUpdate 来处理这种情况:

  componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentDidUpdate(prevProps) {
    // Unsubscribe from the previous friend.id
    ChatAPI.unsubscribeFromFriendStatus(
      prevProps.friend.id,
      this.handleStatusChange
    );
    // Subscribe to the next friend.id
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

忘记正确处理 componentDidUpdate 是 React 应用程序中常见的 bug 。

现在考虑使用 Hooks 的这个组件的版本:

function FriendStatus(props) {
  // ...
  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

它不会受到这个bug的影响。 (但我们也没有对它做任何改动。)

没有用于处理更新的特殊代码,因为默认情况下 useEffect 会处理它们。它会在应用下一个 effects 之前清除之前的effects 。为了说明这一点,这里是一个订阅和取消订阅调用的序列,该组件可以随着时间的推移产生:

// Mount with { friend: { id: 100 } } props
ChatAPI.subscribeToFriendStatus(100, handleStatusChange);     // Run first effect

// Update with { friend: { id: 200 } } props
ChatAPI.unsubscribeFromFriendStatus(100, handleStatusChange); // Clean up previous effect
ChatAPI.subscribeToFriendStatus(200, handleStatusChange);     // Run next effect

// Update with { friend: { id: 300 } } props
ChatAPI.unsubscribeFromFriendStatus(200, handleStatusChange); // Clean up previous effect
ChatAPI.subscribeToFriendStatus(300, handleStatusChange);     // Run next effect

// Unmount
ChatAPI.unsubscribeFromFriendStatus(300, handleStatusChange); // Clean up last effect

此行为默认确保一致性,并防止由于缺少更新逻辑而导致类组件中常见的错误。

提示:通过跳过 Effects 来优化性能

在某些情况下,在每次渲染后清理或应用 effect 可能会产生性能问题。在类组件中,我们可以通过在componentDidUpdate 中编写与 prevPropsprevState 的额外比较来解决这个问题:

componentDidUpdate(prevProps, prevState) {
  if (prevState.count !== this.state.count) {
    document.title = `You clicked ${this.state.count} times`;
  }
}

这个要求很常见,它被内置到 useEffect Hook API中。如果在重新渲染之间没有更改某些值,则可以告诉React 跳过应用 effect 。为此,将数组作为可选的第二个参数传递给 useEffect

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes

在上面的例子中,我们传递 [count] 作为第二个参数。这是什么意思?如果 count5 ,然后我们的组件重新渲染,count 仍然等于 5 ,则 React 将比较前一个渲染的 [5] 和下一个渲染的[5] 。因为数组中的所有项都是相同的(5 === 5 ),所以 React 会跳过这个 effect 。这是我们的优化。

当我们使用 count 更新为 6 渲染时,React会将前一渲染中 [5] 数组中的项目与下一渲染中 [6] 数组中的项目进行比较。这次,React将重新运行 effect ,因为 5 !== 6。如果数组中有多个项目,React 将重新运行 effect ,即使其中只有一个不同。

这也适用于具有清理阶段的 effect :

useEffect(() => {
  ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
  return () => {
    ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
  };
}, [props.friend.id]); // Only re-subscribe if props.friend.id changes

将来, 第二个参数可能会通过构建时转换自动添加。