[译]React Hooks-概览

1,084 阅读7分钟

前言

本文为意译,翻译过程中掺杂本人的理解,如有误导,请放弃继续阅读。

原文地址:Hooks at a Glance

正文

Hooks是即将到来的React特性。这个特性能让你不需要编写class component的前提下也能使用state和其他React特性。当前,在React v16.7.0-alpha版本中,它们是可用的。

Hooks是React中一个向后兼容的特性。这篇文档将会为React老手们提供一个Hooks特性的概览。

一. 什么是Hook?

Hooks是一个函数。它能像钩子一样,让你在函数组件的内部也能“钩住”React state,生命周期函数等React特性。Hooks不能在类(classes)里面使用,它能让你在不使用类的前提下来编写React应用。注意,我们并没有推荐你马上就重写现有的组件。但是如果你想的话,你可以在一些新组件上试用一下。

React提供了一些内置的Hook-比如useState。你也可以创建一些自己的custom Hook来在组件间复用某些状态型的逻辑和行为。首先,我们来看看那些内置的Hook。

二. 内置React Hook

1.State Hook

以下示例代码渲染的是counter组件。当你点击按钮的时候,当前的count值就增加1。

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

在这里,useState就是Hook。我们在一个function component里面调用它来为这个组件添加了一个local state-count。React负责在重复渲染过程中保存这个local state。调用useState将会返回了一对值:当前state值和一个负责更新这个state的函数(updater)。你可以在event handler或者别的地方来调用这个updater。它相当于class component中的this.setState。不同的是,Hook返回的这个updater并不会合并新老state。关于这两者(useState所返回的updater和this.state)的比较,我们会在这个章节进行说明。

useState的唯一参数就是state的初始值。在上面的例子中,这个初始值就是0。因为我们的counter是从0开始的。注意,不像this.state,这里的state的初始值并不要求一定是一个javascript字面量对象。不过,如果你喜欢的话,对象也是可以的。Hook的初始state值只会在第一次渲染的时候用到。

除了上述提到的,用Hook生成的state不一定是一个对象外,Hook的state跟class component的state的还有一个不同点。它就是:Hook state可以有多个。也就是说,你可以在一个function component中使用多次state Hook:

function ExampleWithManyStates() {
  // Declare multiple state variables!
  const [age, setAge] = useState(42);
  const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
  const [fruit, setFruit] = useState('banana');
  // ...
}

这里的数组解构语法使得我们可以对调用useState所返回的state变量进行命名。这个命名并不是useState这个API语法的一部分,所以,叫什么都不重要。React在这里作了一个假设。什么假设呢?那就假设你在每一次渲染过程中所调用useState的顺序是一样的。我们稍后再来讨论为什么要这么做。

2.Effect Hook

你以前一定在React组件里面做过数据获取,事件订阅或者DOM操作等行为。我们把这些行为称之为“副作用(side effects)”。这是因为这些行为会影响到其他的组件,并且不能发生在render期间。

effect hook - useEffect,它为function component添加了执行副作用的能力。这在hook特性加入到React之前是办不到的。useEffect这个hook起到的作用跟class component中的componentDidMount,componentDidUpdate和componentWillUnmount等生命周期函数的作用是一样的。不同的是,useEffect将这三个方法的代码合成到一个API里面了。稍后,我们会在使用Effect Hook章节中阐述useEffect和这三个方法的异同。

举个例子,下面这个例子将会在React更新完DOM之后去设置文档的标题:

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

当你调用useEffect的时候,相当于你在告诉React,你想在[将对virtual DOM的更新映射到真实DOM]之后执行你的副作用。副作用声明在组件的内部,这样一来,副作用就能访问组件自身的props和state了。默认情况下,React会在每次渲染之后都会执行你的副作用,这里面包括了第一次渲染。

副作用也可以可选地通过返回一个函数来告知React如何去做一些清除(clean up)副作用的工作。举个例子,下面这个组件使用了一个effect去订阅了某个朋友的在线状态,然后通过取消订阅来做一些清除工作:

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

    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

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

在这里例子中,React将会在组件卸载的时候取消对ChatAPI的订阅。在每次重新渲染之前也是一样的,也会取消对ChatAPI的订阅。如果你不想每次渲染之前后都这样,你也可以告诉React跳过某些订阅-只有props.friend.id变了才重新去订阅。至于如何告诉React跳过某些订阅呢,请查看这里

跟useState一样,你也可以在一个组件的内部多次调用useEffect:

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可以让我们能够根据组件内部的哪块代码是相关的来组织我们的代码(比如说,订阅和取消订阅的代码就是想关联的),而不是像以前那样简单粗暴地逼你只能基于生命周期方法来做代码的分离。

3.其他内置的Hooks

除了,useState和useEffect这两个比较常用的Hook之外,React还提供了一些比较少用但是却很有用的Hook来给大家。比如,useContext就是其中一个。它能让你在不引入额外的组件树层级的情况下订阅到React context:

function Example() {
  const locale = useContext(LocaleContext);
  const theme = useContext(ThemeContext);
  // ...
}

再比如useReducer这个Hook。它能让你用一个reducer去管理组件复杂的本地state:

function Todos() {
  const [todos, dispatch] = useReducer(todosReducer);
  // ...

以上只是抛砖引玉,更多内置的Hook API以及其使用细节请到Hooks API Reference里面查阅。

三. 打造你自己的Hooks

有时候,我们想在组件间复用一些状态型逻辑。传统(Hooks没出来前)的做法是使用高阶组件和render props两种技术来实现对这行状态型逻辑的封装。现在Hook来了。它能让你在不引入额外的组件树层级的前提下达成同样的目标。

在这篇文档的前面部分,我们介绍了一个叫FriendStatus的组件。这个组件的内部通过调用useState和useEffect这两个Hook来实现了对朋友在线状态的订阅。假如我们想在别的组件复用这段订阅逻辑代码,那我们该怎么办呢?

首先,我们应该提取这段逻辑到一个custom Hook里面。我们给这个custom Hook命名为useFriendStatus:

import { useState, useEffect } from 'react';

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

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

  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });

  return isOnline;
}

这个custom Hook接收friendID作为它的参数,最后返回一个boolean值来标识该用户是否在线。

然后,我们就可以像使用 built in Hook一样使用它了:

// 在A组件我们是这样用
function FriendStatus(props) {
  const isOnline = useFriendStatus(props.friend.id);

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

// 在B组件我们又是这样用
function FriendListItem(props) {
  const isOnline = useFriendStatus(props.friend.id);

  return (
    <li style={{ color: isOnline ? 'green' : 'black' }}>
      {props.friend.name}
    </li>
  );
}

以上两个组件的state是完全独立的。 准确地来说,Hooks是关于复用状态型逻辑(stateful logic)而不是复用状态(state)本身的一种方案。实际上,每一次调用Hook所生成的state是完全独立的。所以你完全可以在一个组件内部调用多次同一个custom Hook。

你可以编写自己的custom Hook来覆盖许多常见的业务场景。比如说,表单处理,动画,声明式的订阅,定时器等等。此外,还有很多我们没想得到的场景,你自己也可以想想如何使用custom Hook来完成封装。我们很期待React社区将会推出什么样的custom Hooks!

从另外一个角度来讲,custom Hook更多是一种约定俗成的写法而不是一种特性。如果一个javascript函数的函数名是以“use”开头,并且在函数内部里面调用了其他的Hook(built in Hook或者custom Hook),那么我们就可以称这个函数为custom Hook。之所以约定使用“useSomething”这种命名法,是为了支持linter plugin运用Hooks规则对其进行检查。

关于custom Hooks的更多内容,请查看Building Your Own Hooks

四. Hooks的铁规

Hooks是普通的javascript函数。但是不同于普通的javascript函数,React为Hooks强加了两条额外的规则:

  • 只能在当前作用域的顶层来调用Hooks。不能在循环语句,条件分支语句或者被嵌套函数里面来调用Hooks。
  • 只能在React的function component中调用Hooks。不要在普通的javascript函数里面调用Hooks。注意,还有另外一个地方可以调用Hooks。那就是custom Hooks。在custom Hooks中调用Hooks也要遵循第一条规则。

我们提供了一个linter plugin来自动地检查用户对Hooks的调用是否符合这两条规则。当然,我们也知道这两条规则看起来有点限制过头或者令人困惑的。但是没办法啊,这两条规则是保证Hooks有效发挥作用的必要前提。

你可以在Rules of Hooks专栏里面查阅更多的细节。