最近在写一个前端页面,看了一个 UI 框架的 Example,发现已经看不懂 React 了。上次写 React 的时候,还都是用 Class Based Component, 我还花了一些时间弄明白一个组件的“生命周期”,没想到这么短的时间,已经不流行使用 Class 写组件了。现在用一个叫 React Hooks 的东西,可以通过函数写出来组件。为了看懂现在的 React 代码,我又去学了 Hooks,这边文章来总结我对 Hooks 的理解。
Hooks 就是一些 React 提供的内置函数,通过 Hooks 就可以在 Function 中操作组件的状态(state)了。在我看来解决了两个问题:
- Function 中是不可以定义 state 的,所以以前 Function 只能用来写 stateless 的组件,如果有一天你觉得这个 stateless 的组件要加入状态了,那就必须把它先变成 Class Base Component 才行;
- Class 组件本身也有问题,它这个设计是要求开发者按照组件的生命周期来写代码,
constructor()
-> componentDidMount()
-> componentWillUpdate()
这种方式,按照 React 的逻辑来组织代码,而不是按照代码本身表达的业务逻辑来组织代码。以前每次写一个新的组件的时候,我都要依靠 Vim 的模板功能生成一个代码模板,但是现在不需要了。写 Hooks 几乎没有模板代码。
通过例子认识 Hooks
下面这个例子来自官方的文档。
|
import React, { 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” 了一个 React 的 state:count
。count
的初始值就是 useState()
的参数,即0. 如果要改变 count
的值,就使用 setCount
这个函数。(之前是使用 this.setState()
)。
可以这么理解,useState()
函数就关联了一个 React 的 state,调用 useState()
会给你 state 的应用,以及更新它的方法。(Hooks 都使用 “use” 开头,为什么不使用 “create” 呢?因为它只是和一个 state “关联”起来了,只是告诉 React “使用”这个 state。只有在组件第一次 render 的时候才会创建这个 state,在后续的更新中并不会创建了。)
另一个 React 提供的 hook 是 useEffect()
。如它的名字,这个 hook 不是和 state
有关的,而是产生 “effect” 的。类似于 React Class 中的 componentDidMount()
或者 componentWillUpdate()
中。
比如下面这段代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
import React, { 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()
收到的函数,并且以后这个组件每次 render 的时候也会调用:更新页面的标题。
可以看到这里面没有和 React 组件生命周期相关的函数名字。这意味着我们可以根据业务逻辑来组织代码,可以将 effect state 等相关的逻辑放在一起,而不是把多个不相关的业务逻辑,都放到 componentDidMount()
里面去。
之前的这种按照组件生命周期来组织代码的方式,很容易出 Bug。比如,下面是一段正确的代码。这个组件订阅了朋友的在线状态,当组件 Unmount 的时候,会需要取消对这个朋友的状态订阅:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
|
class FriendStatus extends React.Component { constructor(props) { super(props); this.state = { isOnline: null }; this.handleStatusChange = this.handleStatusChange.bind(this); } componentDidMount() { ChatAPI.subscribeToFriendStatus( this.props.friend.id, this.handleStatusChange ); } componentWillUnmount() { ChatAPI.unsubscribeFromFriendStatus( this.props.friend.id, this.handleStatusChange ); } handleStatusChange(status) { this.setState({ isOnline: status.isOnline }); } render() { if (this.state.isOnline === null) { return 'Loading...'; } return this.state.isOnline ? 'Online' : 'Offline'; } } |
但其实 componentWillUnmount 这个函数非常容易忘记(尤其是在逻辑越来越多的情况下),造成内存泄漏。
如果用 useEffect()
来实现的话,就比较清晰了,因为这个函数接受的参数是 Effect 的函数,Effect 函数的返回值可以是一个 clean up 的函数。(好像比较绕,这个设计确实有些奇怪,为什么不将 clean up 的函数作为 useEffect
的第二个参数呢?)
用 useEffect()
来写的话,就是下面这种形式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
import React, { useState, useEffect } from 'react'; function FriendStatus(props) { const [isOnline, setIsOnline] = useState(null); useEffect(() => { function handleStatusChange(status) { setIsOnline(status.isOnline); } 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'; } |
把更新的函数传给 useEffect()
,然后在自己的函数里面返回一个 callback 用来 cleanup. 因为这些逻辑都是在一起的,所以更加不容易忘记。
另外,useEffect()
会在组件 Mount 以及每次更新的时候都运行,相当于 componentDidMount()
和 componentWillMount()
两个函数合起来了。
Hooks 的原理
一个函数中可以使用 Hooks 多次,用来关联不同的 state,比如下面这段代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
function Form() { // 1. Use the name state variable const [name, setName] = useState('Mary'); // 2. Use an effect for persisting the form useEffect(function persistForm() { localStorage.setItem('formData', name); }); // 3. Use the surname state variable const [surname, setSurname] = useState('Poppins'); // 4. Use an effect for updating the title useEffect(function updateTitle() { document.title = name + ' ' + surname; }); // ... } |
但是在调用 useState()
的时候并没有告诉 React name
是和哪一个 State
来关联,React 是怎么知道的呢?
答案是调用顺序,你按照这个顺序使用 Hooks,React 就按照这个顺序给你赋予这些 state 的值,“绑定”的过程类似下面这样:
|
// ------------ // First render // ------------ useState('Mary') // 1. Initialize the name state variable with 'Mary' useEffect(persistForm) // 2. Add an effect for persisting the form useState('Poppins') // 3. Initialize the surname state variable with 'Poppins' useEffect(updateTitle) // 4. Add an effect for updating the title // ------------- // Second render // ------------- useState('Mary') // 1. Read the name state variable (argument is ignored) useEffect(persistForm) // 2. Replace the effect for persisting the form useState('Poppins') // 3. Read the surname state variable (argument is ignored) useEffect(updateTitle) // 4. Replace the effect for updating the title |
所以顺序至关重要,知道这一点,就可以避免一些错误的使用方法。比如,一个原则是,只在 Component 的最顶层使用 Hooks,假如你没有函数的最顶层使用 Hooks,而是在嵌套结构(比如循环或者 if block 中)使用,那么绑定的时候就会出问题。
比如像下面这样:
|
// 🔴 We're breaking the first rule by using a Hook in a condition if (name !== '') { useEffect(function persistForm() { localStorage.setItem('formData', name); }); } |
那在 React 实际绑定组件的内部状态的时候,就会乱掉:
|
useState('Mary') // 1. Read the name state variable (argument is ignored) // useEffect(persistForm) // 🔴 This Hook was skipped! useState('Poppins') // 🔴 2 (but was 3). Fail to read the surname state variable useEffect(updateTitle) // 🔴 3 (but was 4). Fail to replace the effect |
另一个原则是,只在 React Function Components 里面使用 Hooks。如果只在 Function Component 里面调用 Hooks 的话,你看到一个 Component 就会知道里面的 State 的变化,但是如果状态还在 Component 外面被控制,那么就很难管理了。并且 React Hooks 应该也不会在 Component 之外去帮你管理这些状态。
定义自己的 Hooks
没有 Hooks 以前,如果要抽象一部分涉及 state 的代码出来复用的话,只能再写一个 Class Component,现在可以用 Hooks 了。我们可以定义一个自己的 Hook。
比如,重用一段 Friend 订阅上下线的逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
import { useState, useEffect } from 'react'; function useFriendStatus(friendID) { const [isOnline, setIsOnline] = useState(null); useEffect(() => { function handleStatusChange(status) { setIsOnline(status.isOnline); } ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange); return () => { ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange); }; }); return isOnline; } |
Hooks 里面会调用其他的 Hook,当然,也只能在最顶层来调用。
我们自己定义的 Hooks 不像是 React 内置的那些一样参数都是固定的,Hooks 本质上就是调用了其他的 Hooks 的函数,所以我们可以自定义自己的参数和返回值:
- 订阅一个朋友的状态:所以接收的参数是
FriendId
- 提供的是朋友的状态,所以返回一个 onlineStatus
在 React 的视角,即使你 call 的是你自己定义的 Hooks,但是最终里面,还是调用的 React 定义的 Hooks。所以最终,你都只调用了 React 提供的 Hooks。就像我们所有的程序调用的函数最终只调用到了系统提供给我们的函数一样。
参考资料:
- https://reactjs.org/docs/hooks-intro.html