React 之 Hook

2,323 阅读6分钟

一、Hook 的简介

什么是 Hook?

Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及生命周期等特性的函数

为什么会有 Hook?

  • 在组件之间复用状态逻辑很难 React 提供了一些方法来实现组件的选择性渲染或复用,如 Render Props高阶组件 等,但这些方法组成的组件也带来了“嵌套地狱”的问题,复杂了组件的结构,增大了阅读理解代码的难度。React 提供了自定义 Hook 来解决上面提到的问题。

  • 复杂组件变得难以理解 在一些复杂的组件中往往都包含了较多的状态逻辑和方法,如组件常常在 componentDidMount 中获取数据。但是,同时在该生命周期中也包含很多其它的逻辑,如设置事件监听,而之后需在 componentWillUnmount 中清除。一些原本没关联的的代码因为修改了相同的状态而冗杂在一起。当然 React 也引入了 Redux 用来实现状态管理,但这也引入了很多抽象概念,文件也相对应复杂起来。Hook 将组件中相互关联的部分拆分成更小的函数,而并非强制按照生命周期划分。

  • 难以理解的 class 在组件中使用 class 时,开发者必须去理解 JavaScript 中 this 的工作方式,需要处理事件绑定等等,另外,class 还存在不能很好的压缩等问题。Hook 使你在非 class 的情况下可以使用更多的 React 特性。

什么时候用 Hook?

在编写函数组件并意识到需要向其添加一些 state,以前的做法是必须将其它转化为 class。现在你可以在现有的函数组件中使用 Hook。

二、Hook 的使用

State Hook

useState 是一个 State Hook ,用来增加组件中的 state ,下面分别通过 class 和 useState 两种写法实现一个计数器。

class 实现方式:

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>
    );
  }
}

state 初始值为 { count: 0 } ,当点击按钮后,通过调用 this.setState() 来增加 state.count。

Hook 实现方式:

import React, { useState } from "react";

function Example() {
  // 声明一个叫 "count" 的 state 变量
  const [count, setCount] = useState(0);

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

在组件中调用 useState Hook,定义一个 “state 变量”。自定义变量 count,它与 class 里面的 this.state 提供的功能完全相同。useState() 方法里面唯一的参数就是初始 state ,此处传值为 0 ,它返回值为一个数组,包括了当前 state 以及更新 state 的函数的,分别对应这里的 count 和 setCount 。 如果需要使用多个 state 变量只需要执行对应数量的 useState Hook 即可。

Effect Hook

在 React 组件中执行数据获取、订阅或者手动修改过 DOM,这些操作称为“副作用”,简称为“作用”。

useEffect 一个 Effect Hook,给函数组件增加了操作副作用的能力。它跟 class 组件中的 componentDidMount、componentDidUpdate 和 componentWillUnmount 具有相同的用途,只不过被合并成了一个 API。

当使用 useEffect 时,就是在告诉 React 在完成对 DOM 的更改后运行“副作用”函数。由于副作用函数是在组件内声明的,所以它们可以访问到组件的 props 和 state。默认情况下,React 会在每次渲染后调用副作用函数,包括第一次渲染的时候。

下面为上面的计数器增加了一个小功能:将 document 的 title 设置为包含了点击次数的消息。

class 的实现:

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>
    );
  }
}

Hook 实现方式:

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

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

  // 作用同 componentDidMount and componentDidUpdate:
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

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

通过上面两种实现方式,不难发现,在 class 中,如果我们想在组件加载和更新时执行同样的操作,需要在两个生命周期函数中编写重复的代码。

而使用 Hook 则不再需要考虑组件此时是“挂载”还是“更新”,默认情况下,useEffect 在第一次渲染之后和每次更新之后都会执行。在组件内部中使用 useEffect 时,需要传一个函数 “effect” ,在函数中可直接访问 state 或 props,然后告诉 React 组件需要在渲染后执行某些操作。useEffect 也可接受第二个参数

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

这个参数可通过跳过 Effect 进行性能优化,传入的 [count] 会和前一次渲染的 count 进行对比,如果相同则跳过本次 effct,作用类似于在 componentDidUpdate 中添加对 prevProps 或 prevState。

Hook 也是允许我们清除 effect ,类似生命周期 componentWillUnmount ,下面为我们的计数器增加一个定时器,并且在组件卸载的时候清楚这个定时器。

class 实现方式:

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

  componentDidMount() {
    let _this = this;
    _this.interval = setInterval(() => {
      _this.setState({
        count: count++
      });
    }, 1000);

    document.title = `count is ${this.state.count}`;
  }

  componentDidUpdate() {
    document.title = `count is ${this.state.count}`;
  }

  componentWillUnmount() {
    clearInterval(this.interval);
  }

  render() {
    return (
      <div>
        <p>count is {this.state.count}</p>
      </div>
    );
  }
}

Hook 实现方式:

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

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

  useEffect(() => {
    document.title = `count is ${count}`;
    interval = setInterval(() => {
      setCount(count + 1);
    }, 1000);

    //若存在,则清除 effect 操作
    return clearInterval(interval);
  });

  return (
    <div>
      <p>count is {count}</p>
    </div>
  );
}

在 componentDidMount 和 componentWillUnmount 之间是相互对应的,两者作用于相同的副作用,即开启定时器和清除定时器,但此时两者的逻辑是拆分开来写的。

在 Hook 中,清除 effect 是设计在同一个地方执行的,当 effect 返回一个函数,React 将会在执行清除操作时调用它,即在卸载组件时执行 clearInterval ,保证了同一个 effect 的逻辑可以放在一起。

自定义 Hook

通过自定义 Hook,可以将组件公共逻辑提取到可重用的函数中。

前面我们给计数器加了定时器,每秒+1,如果我们想按每秒+2 或者其他规律修改 count 的话,我们可以修改计算规则,这里举例+2:

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

function ExampleB() {
  const [count, setCount] = useState(0);
  let interval = null;

  useEffect(() => {
    document.title = `count is ${count}`;
    interval = setInterval(() => {
      setCount(count + 2);
    }, 1000);

    //若存在,则清除 effect 操作
    return clearInterval(interval);
  });

  return (
    <div>
      <p>count is {count}</p>
    </div>
  );
}

很明显,这里只是修改了一个地方,两个组件有公共的逻辑,因此我们可以使用自定义 Hook 将公共的部分抽离出来,Hook 是可以让我们不增加组件的情况下解决相同问题。

提取自定义 Hook

自定义 Hook 其实就是个内部使用了 useState 、useEffect 的普通函数,其名称以 “use” 开头,可以将组件逻辑提取到可重用的函数中;

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

function useSetCount(c,n) {
  const [count, setCount] = useState(c);
  let interval = null;

  useEffect(() => {
    interval = setInterval(() => {
      setCount(count + n);
    }, 1000);

    return clearInterval(interval);
  });

  return count;
}

与 React 组件不同的是,自定义 Hook 不需要具有特殊的标识。我们可以自由的决定它的参数是什么,以及它应该返回什么(如果需要的话)。换句话说,它就像一个正常的函数。但是它的名字应该始终以 use 开头。 此处自定义了一个 useSetCount Hook,接受 count 和计算数值,并返回计算结果。

使用自定义 Hook

我们可以在需要的组件中使用自定义的 useSetCount ,比如:

function ExampleA(props) {
  const count = useSetCount(1, 1);

  return (
    <div>
      <p>count is {count}</p>
    </div>
  );
}
function ExampleB(props) {
  const count = useSetCount(10, 10);

  return (
    <div>
      <p>count is {count}</p>
    </div>
  );
}

注意:

  • 自定义 Hook 必须以 “use” 开头。 如果不遵循的话,由于无法判断某个函数是否包含对其内部 Hook 的调用,React 将无法自动检查你的 Hook 是否违反了 Hook 的规则。
  • 两个组件中使用相同的 Hook 不会共享 state 自定义 Hook 是一种重用状态逻辑的机制,所以每次使用自定义 Hook 时,其中的所有 state 和副作用都是完全隔离的。

三、Hook 规则

Hook 本质就是 JavaScript 函数,但是在使用它时需要遵循两条规则:

  • 只在最顶层使用 Hook 不要在循环,条件或嵌套函数中调用 Hook, 确保总是在你的 React 函数的最顶层调用他们。遵守这条规则,就能确保 Hook 在每一次渲染中都按照同样的顺序被调用。这让 React 能够在多次的 useState 和 useEffect 调用之间保持 hook 状态的正确。
  • 只在 React 函数中调用 Hook 不要在普通的 JavaScript 函数中调用 Hook ,遵循此规则,确保组件的状态逻辑在代码中清晰可见。