React Hooks

前言

React Conf 2018 上 React 提出了关于 React Hooks 的提案,Hooks 作为 React v16.7.0-alpha 中加入的新特性引起了广泛的讨论,这篇文章主要描述了 Hooks 的基础使用,社区的一些讨论以及个人的一些思考。
基础使用部分来源于官方文档
讨论来源于社区以及官方仓库中的 RFC

Hooks 使用概览

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

这里我们用数组解构的 count, setCount 替代了传统的 this.state.count, this.setState({count: xx}) , 可以注意到这里是一个 Example 的函数而非传统的 Component 子类,在这个提案,Hooks 并不适用于传统的类中,这点在之后的讨论也会提到。

上面只是定义了一组 state 变量获取以及设置,同样我们可以在一个函数里定义多组:

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

看完两个 useState 的使用再来思考,到底什么是 Hooks :

Hooks are functions that let you “hook into” React state and lifecycle features from function components.

所以我们就知道了 Hook 就是一个我们不需要关注组件生命周期的设置状态的特性,而且可以使我们能够脱离类来使用 React.

Effect Hook

在使用之前的 React 组件时,我们可能经常会在 Component 的生命周期方法中做一些数据获取,状态订阅以及手动修改 DOM 的操作,那么在不使用 Component 类的函数中如何使用 Hook 来完成相应的实现呢:

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 会在组件每次渲染的时候(包括第一次)调用 useEffect 中的函数来更新 document.title, 我们还可以通过返回一个函数来做一次清理工作:

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

我们一般在 componentWillUnmount 中做一些清理操作,这里可以直接在 useEffect 中返回清理函数来交给 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);
  }
  // ...

Custom Hooks

通常情况,我们希望重用一些带状态逻辑的组件,在以前的 React 中会使用 高阶组件 或者 render props

上面那个例子,我们可以抽象出一个单独的 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;
}

那么使用上我们可以:

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

这样关于状态的逻辑就完全从组件中抽离出来成为独立的一部分,

Hooks are a way to reuse stateful logic, not state itself.

Hooks 更关注的是状态以及状态逻辑的剥离,复用,使之前散落在各处的状态逻辑从组件的生命周期及其他方法中脱离出来。

如果函数以 'use' 开头并且调用其他 Hooks, 我们就认为它是一个自定义的 Hook,useXXX 这种命名约定也会帮助 ESLinter 来查找使用 Hooks 中的错误 (稍后会介绍 linter 规则的使用)。

Other Hooks

useContext 允许订阅 React 上下文而不引入嵌套

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

useReducer 允许使用 reducer 管理复杂组件的本地状态:

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

Hooks 规范

使用 Hooks 也不是完全没有限制的,需要遵守一些规则:

在函数顶层调用 Hooks

不要在循环,条件判断,或者高阶函数中使用 Hooks.

只在 React 函数中调用 Hooks

不要在一般的 Javascript 函数中调用 Hooks, 你只可以在 React 函数组件中调用或者从自定义的 Hook 中调用其他的 Hooks.

ESLint Plugin

官方插件安装:

npm install eslint-plugin-react-hooks@next

然后在 .eslintrc 中加入:

// Your ESLint configuration
{
  "plugins": [
    // ...
    "react-hooks"
  ],
  "rules": {
    // ...
    "react-hooks/rules-of-hooks": "error"
  }
}

一些讨论及 QA

需要重写类组件吗?

官方给的回复是我们不建议重写之前的类组件,而是在新的代码中使用 Hooks.

已有的 React 知识中跟 Hooks 有多少相关性?

Hooks 是使用已经知道的 React 功能的更直接的方式 - 例如状态,生命周期,上下文和引用。 它们并没有从根本上改变 React 的工作方式,而且对组件,props 和自上而下数据流的了解也同样重要。

Hooks 会替代 render props 以及高阶组件吗?

我们经常使用 render props 和 HOC 只渲染一个子节点,对于此场景来说,Hooks 是一个更好的服务于此的方式, 仍然有场景适用于这两者,比如虚拟滚动组件可能有 renderitem prop 或者可视容器组件有它自己的 DOM 结构。但在大多数情况下, Hooks 就足够了,可以帮助减少树中的嵌套。

Hooks 对 Redux connect() 和 React Router 等 API 意味着什么?

可以继续使用那些 API, 在未来, 这些库的新版本可能会有些自定义的 Hooks 比如 useRedux()useRouter() 来提供相同的功能。

Hooks 能够使用静态类型吗

Hooks 是被设计来使用静态类型的。因为它们是函数,所以相比那些高阶组件更容易进行类型修正。React 团队已提前与 Flow 和 TypeScript 团队联系,他们计划在未来包含React Hooks的定义。

如何测试使用了 Hooks 的组件

从 React 团队的观点来看,一个使用了 Hooks 的组件与一个一般的组件没有差别,所以测试来说也没有任何差别(如果测试没有基于 React 内部实现的话)。

最后

一些社区的声音:“与内部原理的更亲密接触”, “比原先更细粒度的逻辑组织与复用”,“有状态的组件没有渲染,有渲染的组件没有状态。”

这里面我最喜欢出自 对 React Hooks 的一些思考 这篇文章的

有状态的组件没有渲染,有渲染的组件没有状态。

我们可以理解 Hooks 为一个状态组件,而减少传统 React Component 中的状态相关逻辑,能够使状态及状态逻辑进行复用,减少以前的那些胶水方法,减少一些为了传递状态造成的“难看的”代码。

尤大在看完之后,也撸了一个 vue-hooks

补一个 Dan 在演讲时喷口水的场景 :p

-- EOF --
以上为本篇博客的全部内容,欢迎提出建议和指正,
个人联系方式详见关于

Comments
Write a Comment