11 min read

React Hooks (Proposal)

在 React v16.7.0 alpha 版本里,提出了一个新的 Feature Proposal: Hooks ,对社区以及未来 React 周边生态发展所带来的影响是巨大的。

学习 Hooks 的知识需要对 React 生态有较深入的理解

What is the Hooks ?

Hooks 是 React 内部组件中的一系列特殊函数,直观带来的改变是引入 state、side effects function、或者其他 React 功能,无需使用 Classes 编写组件(类语法带来的问题有很多),或多或少为前端带来更普世的 functional programming 思想。

引入 Hooks 的动机

React 官方阐明了引入 Hooks 的动机,Hooks 出现前,我们编写 React 组件 会经常遇到的问题:

  1. It’s hard to reuse stateful logic between components
    • React 没有提供官方方案去解决 组件之间共享复用有状态逻辑 ,组件间逻辑的复用和数据传递就变得十分困难(必须一层一层往下传),所以我们使用 render propshigher-order components 来解决复用逻辑的同时引来了新的问题,一些无关 UI 的 wrapper 组件越来越多,嵌套组件越来越深,形成 wrapper hell ,虽然 React devTools 有过滤器来帮助我们更容易地调试。
    • 使用 Hooks 可以在不改变组件层次结构的情况下复用有状态逻辑。可以利用 custom hooks,复用包含状态的逻辑,这些逻辑不再出现在组件树中,而是形成一个独立、可测试的单元,但仍然响应 React 在渲染之间的变化;社区之间分享 自定义 hooks 更容易,hooks 就像插件一样。
  2. Complex components become hard to understand
    • 随着项目深入,我们逐渐会编写越来越复杂的逻辑在组件中,这导致了再生命周期函数内编写的逻辑非常臃肿,例如 添加监听器,我们需要在componentDidMountcomponentWillUnmount 中分别编写添加与删除监听器的逻辑,而一般在 componentDidMount 中,我们也会编写 请求数据 的逻辑。各种功能不相关联的逻辑写在一起,而且相同功能的逻辑散落在不同函数内,这带来许多隐患以及调试上的困难
    • 使用 Hooks 可以 将相关联的逻辑 code 由组件拆分出来成更简单直观的函数(例如订阅事件、请求数据)
  3. Classes confuse both people and machines
    • React 官方认为 JS 的 Class 语法的学习成本很高,使用类语法,要必须清楚 this 在 JS 的工作方式,例如我们需要 绑定事件处理程序 (以何种方式绑定这里不是重点,个人推荐箭头函数形式);另外一些重要实践上,使用 Class 语法也带来诸多问题,详细参阅 classes-confuse-both-people-and-machines)
    • 使用 Hooks 可以 在无需编写 Class 语法的情况下 引入 state、生命周期函数、或者其他 React 功能

实际上引入 Hooks 并不会给现有的代码带来问题

  1. 完全可选(将使用 Hooks 的选择权交给开发者)
  2. 向后兼容(不会有任何破坏性更改)
  3. 在可预见的未来内,不会从 React 中删除 类语法
  4. Hooks 并没有颠覆之前的 React 概念。相反,带来更直观的 API 实现相同的功能

编写 Hooks

目前 Hooks 的几种主流应用:

  1. State hooks
  2. Effect hooks
  3. Custom hooks (自定义 hooks 用来复用包含状态的逻辑)

useState

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

使用 state hooksfunction components中可以像上面代码这样,等同于 Class 语法的代码就不贴了。

值得一提的是,在 Hooks 出现之前,我们通常叫这样形式的组件为 stateless components or stateless function components ,但现在,有了 Hooks ,我们可以在这类组件中使用 state,所以改称 function components

  1. useState 的参数是 我们需要定义的 state 名的初始值(不必像以前一样,state 必须为 Object,如果我们想要创建两个 state,就调用两次 useState)
  2. 返回值是包含两个值的数组,两个值分别为 当前状态更新它的函数 。(这里我们使用 array destructuring 的方式将值取出来。)

创建多个 state 就像这样

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

this.setState 不同,更新状态总是替换它而不是合并它(也解决了很多之前合并带来的问题)

Functional updates

如果新的 state 值是依赖上一个 state 值来计算的,我们可以给 setState 传递一个函数参数,这个函数的参数为上一个 state 的值,返回值是更新后的 state 值,例如:

function Counter({ initialCount }) {
const [count, setCount] = useState(initialCount);
return (
<>
Count: {count}
<button onClick={() => setCount(0)}>Reset</button>
<button onClick={() => setCount((prevCount) => prevCount + 1)}>+</button>
<button onClick={() => setCount((prevCount) => prevCount - 1)}>-</button>
</>
);
}

所以如果需要更新的 state 值为 Object,我们应该使用 object spread syntax

setState((prevState) => {
// Object.assign would also work
return { ...prevState, ...updatedValues };
});

延迟初始化 state

如果初始化的值是需要大量计算得到的结果,可以使用函数代替,此函数只会在初始化阶段执行

const [state, setState] = useState(() => {
const initialState = someExpensiveComputation(props);
return initialState;
});

useEffect

Effect 其实就是 请求数据,操作 DOM,以及订阅事件等一系列 副作用/效果

而 useEffect 可以完成的工作则是 之前 componentDidMountcomponentDidUpdatecomponentWillUnmount 的结合(但两者的 Render Model 完全不一样)

React 组件中有两种常见的 Effect:需要清理和不需要清理的 Effect

不需要清理的 Effect

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>
);
}
  1. 将在每次渲染后执行 useEffect
  2. useEffect 写在 函数内部是为了直接访问到 state 值,利用了闭包的性质,不需要额外 API

需要清理的 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);
// 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';
}

需要单独的 API 来执行清理逻辑。因为添加和删除订阅的逻辑是相关的,useEffect 旨在将其保持在一起。

如果 useEffect 返回一个函数,React 将在清理时执行它

清理的时机是 当组件卸载时,但 useEffect 会在每次渲染后运行而不仅仅是一次, 这就是 React 在下次执行 useEffect 之前还清除前一个 useEffect 的原因

Using the Effect Hook – React

如果要减少 useEffect 内 并不是每次渲染都必要的逻辑,可以:

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

React 会比较两次渲染的 count 值,如果一样,就会跳过这次 useEffect

Custom Hooks

我们可以封装在多个组件可重用的包含状态的逻辑,例如

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

useFriendStatus 就是一个我们写好的复用逻辑函数,供其他组件调用。

多个组件使用 相同自定义 Hooks,它们的状态和效果是 独立隔离的,仅仅是逻辑的复用。因为本质是 调用 Custom Hooks 是调用 useStateuseEffect,它们在一个组件调用很多次,彼此产生的状态也是完全独立的。

详细参见文档:Writing Custom Hooks – React

使用 Hooks 的规则:

务必遵守的规则:

Rules of Hooks – React

Hooks API:

Hooks API Reference – React

Conclusion

React Hooks 带来的边际效应可以说是巨大的,希望更加完善之后,可以看到打开新窗的前端。

启发 Hooks 的产生:Hooks FAQ – React

关于 Hooks 的讨论:RFC: React Hooks by sebmarkbage · Pull Request #68 · reactjs/rfcs · GitHub

有趣的是,Vue 的作者也很快创建了在 Vue 实验 Hooks 的 repo:GitHub - yyx990803/vue-hooks: Experimental React hooks implementation in Vue

Dan 写了一篇讲解文章: Making Sense of React Hooks