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 组件 会经常遇到的问题:
- It’s hard to reuse stateful logic between components
- React 没有提供官方方案去解决 组件之间共享复用有状态逻辑 ,组件间逻辑的复用和数据传递就变得十分困难(必须一层一层往下传),所以我们使用
render props
和higher-order components
来解决复用逻辑的同时引来了新的问题,一些无关 UI 的 wrapper 组件越来越多,嵌套组件越来越深,形成wrapper hell
,虽然 React devTools 有过滤器来帮助我们更容易地调试。 - 使用 Hooks 可以在不改变组件层次结构的情况下复用有状态逻辑。可以利用 custom hooks,复用包含状态的逻辑,这些逻辑不再出现在组件树中,而是形成一个独立、可测试的单元,但仍然响应 React 在渲染之间的变化;社区之间分享 自定义 hooks 更容易,hooks 就像插件一样。
- React 没有提供官方方案去解决 组件之间共享复用有状态逻辑 ,组件间逻辑的复用和数据传递就变得十分困难(必须一层一层往下传),所以我们使用
- Complex components become hard to understand
- 随着项目深入,我们逐渐会编写越来越复杂的逻辑在组件中,这导致了再生命周期函数内编写的逻辑非常臃肿,例如 添加监听器,我们需要在
componentDidMount
与componentWillUnmount
中分别编写添加与删除监听器的逻辑,而一般在componentDidMount
中,我们也会编写请求数据
的逻辑。各种功能不相关联的逻辑写在一起,而且相同功能的逻辑散落在不同函数内,这带来许多隐患以及调试上的困难 - 使用 Hooks 可以 将相关联的逻辑 code 由组件拆分出来成更简单直观的函数(例如订阅事件、请求数据)
- 随着项目深入,我们逐渐会编写越来越复杂的逻辑在组件中,这导致了再生命周期函数内编写的逻辑非常臃肿,例如 添加监听器,我们需要在
- Classes confuse both people and machines
- React 官方认为 JS 的 Class 语法的学习成本很高,使用类语法,要必须清楚
this
在 JS 的工作方式,例如我们需要 绑定事件处理程序 (以何种方式绑定这里不是重点,个人推荐箭头函数形式);另外一些重要实践上,使用 Class 语法也带来诸多问题,详细参阅 classes-confuse-both-people-and-machines) - 使用 Hooks 可以 在无需编写 Class 语法的情况下 引入 state、生命周期函数、或者其他 React 功能
- React 官方认为 JS 的 Class 语法的学习成本很高,使用类语法,要必须清楚
实际上引入 Hooks 并不会给现有的代码带来问题
- 完全可选(将使用 Hooks 的选择权交给开发者)
- 向后兼容(不会有任何破坏性更改)
- 在可预见的未来内,不会从 React 中删除 类语法
- Hooks 并没有颠覆之前的 React 概念。相反,带来更直观的 API 实现相同的功能
编写 Hooks
目前 Hooks 的几种主流应用:
- State hooks
- Effect hooks
- 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 hooks
在 function components
中可以像上面代码这样,等同于 Class 语法的代码就不贴了。
值得一提的是,在 Hooks 出现之前,我们通常叫这样形式的组件为
stateless components
orstateless function components
,但现在,有了 Hooks ,我们可以在这类组件中使用 state,所以改称function components
。
- useState 的参数是 我们需要定义的 state 名的初始值(不必像以前一样,state 必须为 Object,如果我们想要创建两个 state,就调用两次 useState)
- 返回值是包含两个值的数组,两个值分别为 当前状态 和 更新它的函数 。(这里我们使用
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 可以完成的工作则是 之前 componentDidMount
,componentDidUpdate
和componentWillUnmount
的结合(但两者的 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> );}
- 将在每次渲染后执行 useEffect
- 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
是调用useState
和useEffect
,它们在一个组件调用很多次,彼此产生的状态也是完全独立的。
详细参见文档: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