关于 react hooks 的基本介绍以及动机可以看 React 的文档:https://reactjs.org/docs/hooks-intro.html

一、State Hook

以一个简单的计数器举例,使用 useState 这个 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>
  );
}

上面的 useState 实际上就是一个 Hook,我们在函数组件中调用它来向它添加一些本地的 state,而 React 在 re-render 之前会保留这个 state。useState 返回两个内容:

  • 当前的 state 值
  • 一个允许更新 state 值的 function

可以从事件处理程序或者其他位置调用返回的 function,和类中的 this.setState 比较累色,只不过不会将新的 state 和旧的 state 合并在一起。(React 给过一个使用 useState 和 this.state 的比较的示例https://reactjs.org/docs/hooks-state.html

useState 的唯一参数是初始状态,在上面的例子中,初始 state 是 0,因为计数器从 0 开始。请注意,和 this.state 不同的是,这里的 state 不一定是对象,可以是随便需要的形式。而初始状态参数仅仅在第一次渲染的时候使用。

1、声明多个 state 变量

可以在单个组件中多次使用 state Hook:

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

数组结构语法允许给 useState 的声明的 state 变量赋予不同的名称,这些名称并不是 useState API 的一部分。相反,React 假定如果多次调用 useState,则在每次渲染期间,以相同的顺序执行。

2、什么是 Hook

Hook 是允许从功能组件 挂钩 React 的 state 和生命周期方法等功能。Hook 在类的内部不起作用——允许在没有类的情况下使用 React。(React 不建议全部重写组件,只是建议如果感兴趣 Hook,可以在新的组件中使用)

React 提供了一些像 useState 这样的内置 Hook,还可以创建自己的 Hook 以便于重用不同组件之间的状态行为。

而更多关于 useState 的内容可以看 Using State Hool 的文档

二、Effect Hook

我们已经可以在 React 组件中进行数据请求、subscriptions 或者是手动更改 DOM。这些操作都是称为 side effects,也就是副作用。因为它们会影响其他组件,并且在渲染过程中没有办法完成。

useEffect 增加了从功能组件执行副作用的功能。它与 React 类的 componentDidMountcomponentDidUpdatecomponentWillUnmount 具有相同的效果,但是统一成了一个 API。可以在 Using the Effect Hool 文档中查找关于更多 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 在刷新对 DOM 的更改后运行 effect 副作用方法。因为 副作用方法 是在组件中声明的,因此可以访问 props 和 state。

默认情况下, React 在每次渲染后执行 effect 方法 —— 包括第一次渲染。想查看和生命周期方法的比较,也可以在 Using the Effect Hool 文档中找到。

同时 effect 副作用方法可以通过返回函数指定如何 “清理” 它们。例如,下面组件使用效果来订阅好友的在线 state,并通过取消订阅来实现清理的目的:

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

在这个示例中,组件卸载或者是由于后面的 re-render 而重新执行副作用方法之前,React 会取消订阅 ChatAPI。当然,如果 props 传过来的 id 是没有变化的,也可以通过某种方式来跳过订阅和取消订阅的行为:https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects

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 允许通过哪些部分相关(比如添加和删除订阅)来组织组件中的副作用,而不是基于生命周期方法进行强制拆分。

三、 Hooks 的规则

Hooks 是 JavaScript 方法, 但它们强加了两个额外的规则:

  • 只能在顶层调用 Hooks,不能再循环、条件或者是嵌套方法中调用 Hooks。
  • 仅在 React 功能组件中使用 Hooks。不能再常规的 JavaScript 方法中调用 Hook。

React 提供了一个 linter 插件 来自动执行这些规则。虽然这些规则可能会让开发者比较首先,但是对于 Hooks 的良好运行至关重要。

四、构建自己的 Hook

有时,可能会希望在组件之间重用一些 state 的逻辑,一般之前的做法都是通过高阶组件或者是 render props 来解决。自定义 Hook 能够做到这种需求,而不需要向 tree 中增加更多组件。

前面虽然介绍了 useStateuseEffect 来订阅朋友的在线状态,如果还希望在另一个组件中重用订阅逻辑,首先需要将这个逻辑提取到一个名为 useFriendStatus 的自定义 Hook 中:

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

这个方法参数是 friendID ,返回的是好友是否在线。

而现在可以在两个组件中直接使用这个 Hook:

function FriendStatus(props) {
  const isOnline = useFriendStatus(props.friend.id);
  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}
function FriendListItem(props) {
  const isOnline = useFriendStatus(props.friend.id);
  return (
    <li style={{ color: isOnline ? 'green' : 'black' }}>
      {props.friend.name}
    </li>
  );
}

这些组件的状态是完全独立的,Hook 是重用有 state 逻辑的一种方式,而不是重用 state 本身。事实上,每次调用 Hook 都会有个完全隔离的状态。因此可以在一个组件中使用相同的自定义 Hook 两次。

自定义 Hook 更像是一种约定,而不是一种功能。如果函数的名字以 use 开头,并且调用了其他的 Hook,则就称其为一个自定义 Hook。useSomething 命名约定是为了 linter 插件在代码中查找错误。

五、其他的 Hooks

除了上面的 useStateuseEffect 之外,还有一些其他不太常用的 Hook 也可能很有用。比如 useContext 允许订阅 React 上下文,不去引入嵌套。

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

useReducer 允许使用 reducer 管理复杂的组件 state:

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