Hooks

hooks的作用

钩子(hook)就是 React 函数组件的副效应解决方案,用来为函数组件引入副效应。 函数组件的主体只应该用来返回组件的 HTML 代码,所有的其他操作(副效应)都必须通过钩子引入。

由于副效应非常多,所以钩子有许多种。React 为许多常见的操作(副效应),都提供了专用的钩子。

  • useState():保存状态
  • useContext():保存上下文
  • useRef():保存引用
  • ……

上面这些钩子,都是引入某种特定的副效应,而 useEffect()是通用的副效应钩子 。找不到对应的钩子时,就可以用它。其实,从名字也可以看出来,它跟副效应(side effect)直接相关。

useEffect

useEffect()本身是一个函数,由 React 框架提供,在函数组件内部调用即可。

例如:

1
2
3
4
5
6
7
8
import React, { useEffect } from 'react';

function Welcome(props) {
useEffect(() => {
document.title = '加载完成';
});
return <h1>Hello, {props.name}</h1>;
}

上面例子中,useEffect()的参数是一个函数,它就是所要完成的副效应(改变网页标题)。组件加载以后,React 就会执行这个函数。

useEffect()的作用就是指定一个副效应函数,组件每渲染一次,该函数就执行一次。组件首次在网页 DOM 加载后,副效应函数也会执行。

useEffect指定依赖项

有时候,我们不希望useEffect()每次渲染都执行,这时可以使用它的第二个参数,使用一个数组指定副效应函数的依赖项,只有依赖项发生变化,才会执行。

如果上例改为

1
2
3
4
5
6
function Welcome(props) {
useEffect(() => {
document.title = '加载完成';
}, []);
return <h1>Hello, {props.name}</h1>;
}

便只会在组件首次挂载时执行函数,相当于class component中的componentDidMount

useEffect() 的用途

只要是副效应,都可以使用useEffect()引入。它的常见用途有下面几种。

  • 获取数据(data fetching)
  • 事件监听或订阅(setting up a subscription)
  • 改变 DOM(changing the DOM)
  • 输出日志(logging)

useEffect返回值

副效应是随着组件加载而发生的,那么组件卸载时,可能需要清理这些副效应。

useEffect()允许返回一个函数,在组件卸载时,执行该函数,清理副效应。如果不需要清理副效应,useEffect()就不用返回任何值。

1
2
3
4
5
6
useEffect(() => {
const subscription = props.source.subscribe();
return () => {
subscription.unsubscribe();
};
}, [props.source]);

上面例子中,useEffect()在组件加载时订阅了一个事件,并且返回一个清理函数,在组件卸载时取消订阅。

实际使用中,由于副效应函数默认是每次渲染都会执行,所以清理函数不仅会在组件卸载时执行一次,每次副效应函数重新执行之前,也会执行一次,用来清理上一次渲染的副效应。

useEffect常见问题

使用useEffect应该在依赖的变量中指明定义的函数会用到哪些state或者props

effect 可能会使用一些频繁变化的值。可能会忽略依赖列表中 state,但这通常会引起 Bug:

1
2
3
4
5
6
7
8
9
10
11
12
function Counter() {
const [count, setCount] = useState(0);

useEffect(() => {
const id = setInterval(() => {
console.log(count);
setCount(count + 1); // 这个 effect 依赖于 `count` state
}, 1000);
return () => clearInterval(id);
}, []); // 🔴 Bug: `count` 没有被指定为依赖
return <h1>{count}</h1>;
}

传入空的依赖数组 [],意味着该 hook 只在组件挂载时运行一次,并非重新渲染时。但如此会有问题,在 setInterval 的回调中,count 的值不会发生变化。因为当 effect 执行时,react会创建一个闭包,并将 count 的值被保存在该闭包当中,且初值为 0。每隔一秒,回调就会执行 setCount(0 + 1),因此,count 永远不会超过 1。

指定 [count] 作为依赖列表就能修复这个 Bug,但会导致每次改变发生时定时器都被重置。事实上,每个 setInterval 在被清除前都会调用一次(等于说直接使用 setTimeout就行了)。要解决这个问题,可以使用类似setState函数式更新的操作,不依赖外部count变量,它允许我们指定 state 该如何改变而不用引用state或者props

1
2
3
4
5
6
7
8
9
10
function Counter() {
const [count, setCount] = useState(0);

useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1); // ✅ 在这不依赖于外部的 `count` 变量 }, 1000);
return () => clearInterval(id);
}, []); // ✅ 我们的 effect 不使用组件作用域中的任何变量
return <h1>{count}</h1>;
}

useLayoutEffect vs useEffect

useLayoutEffect 和 useEffect 的传参一致,但有以下区别

  1. 执行时机不同
    useLayoutEffect 的入参函数会在 react 更新 DOM 树后同步调用(其实就是dom树更新,浏览器重绘前会执行useLayoutEffect里的函数,此时比如去拿 style 或者 dom 尺寸,都是游览器即将渲染的那一次的尺寸,而不是现在页面上展示的尺寸),useEffect 为异步调用(useEffect 则肯定是在游览器渲染完后才执行),所以应该尽可能使用标准的 useEffect 以避免阻塞视觉更新

  2. useLayoutEffect 在 development 模式下 SSR 会有警告⚠️

通常情况下 useLayoutEffect 会用在做动效和记录 layout 的一些特殊场景(比如防止渲染闪烁,在渲染前再给你个机会去改 DOM)。一般不需要使用 useLayoutEffect。

React 组件是一个树形结构,且每个节点都是懒计算的(类似于 Thunk 的概念)。当一个节点不需要重新计算(重绘)时,他的子树都不会计算(重绘)。所以我们做性能优化的目标,就是在尽量离根节点近的位置,拦截不必要的节点重算,从而减少重绘的计算量。

useRef

当需要存放一个数据,需要无论在哪里都取到最新状态时,需要使用 useRef,ref 是一种可变数据。
如上文提到的代码段中,因为存在闭包问题,count永远会为1,log永远为0;
可以引入useRef解决这个问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Counter() {
const [count, setCount] = useState(0);
const currentCount = useRef(count);
// 这里在每一次组件重新渲染后更新ref的值
currentCount.current = count;
useEffect(() => {
const id = setInterval(() => {
console.log(currentCount.current)
setCount(currentCount.current + 1); // 这个 effect 依赖于 `count` state
}, 1000);
return () => clearInterval(id);
}, []); // 🔴 Bug: `count` 没有被指定为依赖
return <h1>{count}</h1>;
}

useRef、useState如何决策用哪种来维护状态

useRef 生成的可变对象,因为使用起来就跟普通对象一样,赋值时候 React 是无法感知到值变更的,所以也不会触发组件重绘。利用其与 useState 的区别,我们一般这样区分使用:

  • 维护与 UI 相关的状态,使用 useState

确保更改时刷新 UI

  • 值更新不需要触发重绘时,使用 useRef
  • 不需要变更的数据、函数,使用 useState

比如,需要声明一个不可变的值时,可以这样:

1
const [immutable] = useState(someState);

不返回变更入口函数。useRef 虽然可以借助 TypeScript 达到语法检测上的 immutable,但实际还是 mutable 的。

useContext

context简介:

React.createContext 新建context

1
const MyContext = React.createContext(defaultValue);

创建一个 Context 对象。当 React 渲染一个订阅了这个 Context 对象的组件,这个组件会从组件树中离自身最近的那个匹配的 Provider 中读取到当前的 context 值。

只有当组件所处的树中没有匹配到 Provider 时,其 defaultValue 参数才会生效。此默认值有助于在不使用 Provider 包装组件的情况下对组件进行测试。注意:将 undefined 传递给 Provider 的 value 时,消费组件的 defaultValue 不会生效。

Context.Provider

每个 Context 对象都会返回一个 Provider React 组件,它允许消费组件订阅 context 的变化。

useContext使用

接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider>value prop 决定。

当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值。即使祖先使用 React.memoshouldComponentUpdate,也会在组件本身使用 useContext 时重新渲染。

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
33
34
35
36
37
38
39
40
41
42
import React, { useContext, useState } from "react";
import { ThemeContext, themes } from "./utils/index";

export default function App() {
const [theme, setTheme] = useState(themes.light);
return (
<ThemeContext.Provider
value={{
theme,
toggle: () =>
setTheme(
theme === themes.light ? themes.dark : themes.light
),
}}
>
<Toolbar />
</ThemeContext.Provider>
);
}

function Toolbar() {
return (
<div>
<ThemedButton />
</div>
);
}

function ThemedButton() {
const context = useContext(ThemeContext);
return (
<button
style={{
background: context.theme.background,
color: context.theme.foreground,
}}
onClick={context.toggle}
>
I am styled by theme context!
</button>
);
}

useMemo

useMemo 主要有两个作用:

  1. 缓存一些耗时计算,通过声明计算结果的依赖是否变更,来重用上次计算结果
  2. 保证引用不变,针对下游使用 React.memo 的组件进行性能优化(useCallback 也有一样的作用)

比如,计算耗时的 fibonacci 数列,就可以用 useMemo 来优化在 n 不变的情况下,二次渲染的性能

useMemo(() => { return fibonacci(props.n) }, [props.n]);

useCallback

useCallback 是简化版的 useMemo,方便缓存函数引用。下面的代码是等价的:

1
2
3
const memoCallback = useCallback((...args) => {
// DO SOMETHING
}, [...deps]);
1
2
3
const memoCallback = useMemo(() => (...args) => {
// DO SOMETHING
}, [...deps]);

在没有遇到性能问题时,不要使用 useCallback 和 useMemo,性能优化先交给框架处理解决。手工的微优化在没有对框架和业务场景有深入了解时,可能出现性能劣化。

致命的 useCallback/useMemo(翻译)

useCallback hell问题总结

关于如何减少 useCallback 看 第二天