理解 useMemo 和 useCallback


如果你一直在努力理解 useMemouseCallback,你并不孤单!我已经和很多 React 开发者聊过,他们都对这两个钩子感到困惑。

在这篇博文中,我的目标是澄清这些混乱。我们将学习它们的功能、为什么有用以及如何最大限度地利用它们。

基本概念

好了,我们从 useMemo 开始。

useMemo 的基本思想是,它允许我们在渲染之间“记住”计算过的值。

这个定义需要进一步解释。实际上,它要求我们对 React 的工作机制有一个相当复杂的心理模型!所以我们先来解决这个问题。

React 的主要功能是使我们的 UI 与应用程序状态保持同步。它使用的工具叫做“重新渲染”。

每次重新渲染都是应用程序在某一时刻基于当前状态的 UI 快照。我们可以把它想象成一堆照片,每张照片捕捉到每个状态变量在特定值下的样子。

每次“重新渲染”会生成当前状态下 DOM 应该呈现的样子。在上面的演示中,它显示为 HTML,但实际上是一些 JavaScript 对象。如果你听说过“虚拟 DOM”这个词,就是在说这个概念。

我们不会直接告诉 React 哪些 DOM 节点需要更改。相反,我们告诉 React,基于当前状态,UI 应该是什么样子。通过重新渲染,React 创建了一个新快照,并通过比较快照,像玩“找不同”游戏一样,找出需要更改的地方。

React 本身经过了高度优化,因此一般来说,重新渲染并不是什么大问题。但是在某些情况下,生成这些快照可能需要一段时间。这会导致性能问题,例如用户执行操作后 UI 无法及时更新。

从根本上说,useMemouseCallback 是帮助我们优化重新渲染的工具。它们通过两种方式做到这一点:

    1. 减少在给定渲染中需要完成的工作量。
    1. 减少组件需要重新渲染的次数。

我们一个一个来讨论这些策略。

用例 1:繁重的计算

假设我们正在构建一个工具,帮助用户找到所有介于 0 和 selectedNum(用户提供的值)之间的素数。素数是指只能被 1 和它本身整除的数字,例如 17。

这是一个可能的实现:

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
43
44
45
46
47
48
49
50
51
52
53
54
55
import React from 'react';

function App() {
// 我们将用户选择的数字保存在状态中。
const [selectedNum, setSelectedNum] = React.useState(100);

// 计算 0 到用户选择的数字 `selectedNum` 之间的所有素数:
const allPrimes = [];
for (let counter = 2; counter < selectedNum; counter++) {
if (isPrime(counter)) {
allPrimes.push(counter);
}
}

return (
<>
<form>
<label htmlFor="num">你的数字:</label>
<input
type="number"
value={selectedNum}
onChange={(event) => {
// 为了防止计算机崩溃,我们将最大值设置为 100k
let num = Math.min(100_000, Number(event.target.value));
setSelectedNum(num);
}}
/>
</form>
<p>
在 1 和 {selectedNum} 之间有 {allPrimes.length} 个素数:
<span className="prime-list">
{allPrimes.join(', ')}
</span>
</p>
</>
);
}

// 判断给定数字是否为素数的辅助函数。
function isPrime(n) {
const max = Math.ceil(Math.sqrt(n));

if (n === 2) {
return true;
}

for (let counter = 2; counter <= max; counter++) {
if (n % counter === 0) {
return false;
}
}

return true;
}
export default App;

我不希望你逐行阅读代码,所以这里是关键点:

  • 我们有一个状态变量 selectedNum,保存用户选择的数字。
  • 使用 for 循环,我们手动计算 0 到 selectedNum 之间的所有素数。
  • 渲染一个受控的数字输入框,让用户可以更改 selectedNum
  • 展示我们计算出的所有素数。

这段代码需要进行大量计算。如果用户选择了一个较大的 selectedNum,我们将需要遍历数万个数字,检查它们是否是素数。即使有更高效的素数检查算法,这种计算始终会消耗大量资源。

我们确实需要在某些时候执行这个计算,比如当用户选择了一个新的 selectedNum 时。但如果我们不必要地重复这些工作,就可能会遇到性能问题。

例如,假设我们的示例还包含一个数字时钟:

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import React from 'react';
const allPrimes = [];
for (let counter = 2; counter < selectedNum; counter++) {
if (isPrime(counter)) {
allPrimes.push(counter);
}
}

return (
<>
<p className="clock">
{format(time, 'hh:mm:ss a')}
</p>
<form>
<label htmlFor="num">你的数字:</label>
<input
type="number"
value={selectedNum}
onChange={(event) => {
let num = Math.min(100_000, Number(event.target.value));
setSelectedNum(num);
}}
/>
</form>
<p>
在 1 和 {selectedNum} 之间有 {allPrimes.length} 个素数:
<span className="prime-list">
{allPrimes.join(', ')}
</span>
</p>
</>
);
}

function useTime() {
const [time, setTime] = React.useState(new Date());

React.useEffect(() => {
const intervalId = window.setInterval(() => {
setTime(new Date());
}, 1000);

return () => {
window.clearInterval(intervalId);
}
}, []);

return time;
}

function isPrime(n) {
const max = Math.ceil(Math.sqrt(n));

if (n === 2) {
return true;
}

for (let counter = 2; counter <= max; counter++) {
if (n % counter === 0) {
return false;
}
}

return true;
}

export default App;

我们的应用程序现在有两个状态变量:selectedNumtime。每秒,time 变量都会更新一次,以反映当前时间,并使用该值在右上角渲染一个数字时钟。

问题是:每当这两个状态变量中的任何一个发生变化时,我们都会重新执行所有昂贵的素数计算。而且由于时间每秒都会变化,这意味着即使用户没有更改数字,我们也在不断地重新生成素数列表!

在 JavaScript 中,我们只有一个主线程,通过每秒反复运行这段代码,使其非常繁忙。这意味着当用户尝试执行其他操作时,应用程序可能会感觉到缓慢,特别是在低端设备上。

但如果我们能“跳过”这些计算呢?如果我们已经拥有某个数字的素数列表,为什么不重复使用这个值,而是每次都从头开始计算?

这正是 useMemo 所允许我们做的。来看一下它的用法:

1
2
3
4
5
6
7
8
9
const allPrimes = React.useMemo(() => {
const result = [];
for (let counter = 2; counter < selectedNum; counter++) {
if (isPrime(counter)) {
result.push(counter);
}
}
return result;
}, [selectedNum]);

useMemo 接受两个参数:

  1. 一段需要执行的工作,封装在一个函数中。
  2. 一个依赖项列表。

在组件首次挂载时,React 会调用这个函数来执行所有逻辑,计算所有素数。函数的返回值会被赋给 allPrimes 变量。

对于后续的每次渲染,React 需要做一个决定:

  • 是重新调用该函数以重新计算值,还是
  • 复用上次计算的结果。

为了做出这个决定,React 会查看提供的依赖项列表。依赖项自上次渲染以来是否发生了变化?如果是,React 将重新运行函数来计算新值。否则,它将跳过所有这些工作,并复用之前计算的值。

useMemo 本质上像一个小型缓存,依赖项则是缓存失效策略。

在这种情况下,我们的意思是:“仅当 selectedNum 发生变化时才重新计算素数列表。”当组件由于其他原因重新渲染(例如 time 状态变量的变化)时,useMemo 会忽略函数并传递缓存的值。

这种技术通常被称为“记忆化”,这也是该钩子被称为 useMemo 的原因。

以下是该解决方案的一个在线版本:

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
import React from 'react';
import format from 'date-fns/format';

function App() {
const [selectedNum, setSelectedNum] = React.useState(100);
const time = useTime();

const allPrimes = React.useMemo(() => {
const result = [];

for (let counter = 2; counter < selectedNum; counter++) {
if (isPrime(counter)) {
result.push(counter);
}
}

return result;
}, [selectedNum]);

return (
<>
<p className="clock">
{format(time, 'hh:mm:ss a')}
</p>
<form>
<label htmlFor="num">你的数字:</label>
<input
type="number"
value={selectedNum}
onChange={(event) => {
let num = Math.min(100_000, Number(event.target.value));
setSelectedNum(num);
}}
/>
</form>
<p>
在 1 和 {selectedNum} 之间有 {allPrimes.length} 个素数:
<span className="prime-list">
{allPrimes.join(', ')}
</span>
</p>
</>
);
}

function useTime() {
const [time, setTime] = React.useState(new Date());

React.useEffect(() => {
const intervalId = window.setInterval(() => {
setTime(new Date());
}, 1000);

return () => {
window.clearInterval(intervalId);
}
}, []);

return time;
}

function isPrime(n) {
const max = Math.ceil(Math.sqrt(n));

if (n === 2) {
return true;
}

for (let counter = 2; counter <= max; counter++) {
if (n % counter === 0) {
return false;
}
}

return true;
}

export default App;
另一种方法

所以,useMemo 确实可以帮助我们避免不必要的计算……但它真的是这里的最佳解决方案吗?

我们经常可以通过重新组织应用程序的结构来避免使用 useMemo

以下是我们可以做的一种方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React from 'react';

import Clock from './Clock';
import PrimeCalculator from './PrimeCalculator';

function App() {
return (
<>
<Clock />
<PrimeCalculator />
</>
);
}

export default App;

我将两个新组件 ClockPrimeCalculator 提取出来。通过从 App 中分离出来,这两个组件各自管理自己的状态。一个组件的重新渲染不会影响另一个组件。

这是显示这种动态关系的图表。每个框代表一个组件实例,当它们重新渲染时会闪烁。可以尝试点击“Increment”按钮来查看效果:

我们常常听到“提升状态”的说法,但有时候,更好的方法是“下推状态”! 每个组件应该有单一的责任,而在上述示例中,App 做了两件完全无关的事情。

当然,这并不总是一个选择。在一个大型的真实应用程序中,有许多状态需要提升到较高的层级,不能被下推。

我在这种情况下还有另一个技巧。

让我们看一个例子。假设我们需要将 time 变量提升到 PrimeCalculator 之上:

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
43
44
45
46
47
48
49
50
51
52
53
import React from 'react';
import { getHours } from 'date-fns';

import Clock from './Clock';
import PrimeCalculator from './PrimeCalculator';

// 将 PrimeCalculator 转换为纯组件:
const PurePrimeCalculator = React.memo(PrimeCalculator);

function App() {
const time = useTime();

// 根据时间选择合适的背景色:
const backgroundColor = getBackgroundColorFromTime(time);

return (
<div style={{ backgroundColor }}>
<Clock time={time} />
<PurePrimeCalculator />
</div>
);
}

const getBackgroundColorFromTime = (time) => {
const hours = getHours(time);

if (hours < 12) {
// 早晨使用浅黄色
return 'hsl(50deg 100% 90%)';
} else if (hours < 18) {
// 下午使用暗蓝色
return 'hsl(220deg 60% 92%)'
} else {
// 晚上使用深蓝色
return 'hsl(220deg 100% 80%)';
}
}

function useTime() {
const [time, setTime] = React.useState(new Date());

React.useEffect(() => {
const intervalId = window.setInterval(() => {
setTime(new Date());
}, 1000);

return () => {
window.clearInterval(intervalId);
}
}, []);

return time;
}

更新图表

这里是更新后的图表,显示发生了什么:

像一个力场一样,React.memo 将我们的组件包裹起来,保护它不受无关更新的影响。我们的 PurePrimeCalculator 仅在接收到新数据或其内部状态更改时重新渲染。

这被称为纯组件。基本上,我们是在告诉 React,这个组件在相同输入下总会产生相同的输出,我们可以跳过没有变化的重新渲染。

我在最近的博文《为什么 React 会重新渲染》中详细讨论了 React.memo 的工作原理。

更传统的方法
在上述示例中,我将 React.memo 应用到了导入的 PrimeCalculator 组件上。
实际上,这有点不寻常。我选择这样结构化,以便在同一个文件中看到所有内容,从而更容易理解。
在实际应用中,我通常将 React.memo 应用于组件导出,如下所示:

1
2
3
4
5
>// PrimeCalculator.js
>function PrimeCalculator() {
/* 组件逻辑 */
>}
>export default React.memo(PrimeCalculator);

现在我们的 PrimeCalculator 组件将始终是纯的,而我们在使用它时不必进行调整。
如果我们需要非纯版本的 PrimeCalculator,我们可以将底层组件作为命名导出。我认为我从未需要这样做。

这里有一个有趣的观点转变:之前我们是对特定计算的结果进行记忆化,计算素数。然而在这个例子中,我对整个组件进行了记忆化。

无论哪种方式,昂贵的计算工作只会在用户选择新的 selectedNum 时重新运行。但我们优化了父组件,而不是特定的慢代码行。

我并不是说一种方法比另一种更好;每种工具都有其在工具箱中的位置。但在这个特定的案例中,我更喜欢这种方法。

现在,如果你曾经尝试在真实环境中使用纯组件,你可能注意到一个奇特的现象:即使看起来没有变化,纯组件通常也会重新渲染很多次!😬

这为我们引出了 useMemo 解决的第二个问题。

更多替代方案
在他的博文“在你使用 memo() 之前”,Dan Abramov 分享了另一种基于使用子组件重构应用程序的方法,以避免需要进行任何记忆化。
感谢 Yuval Shimoni 提出这一点!

使用案例 2:保留引用

在下面的示例中,我创建了一个 Boxes 组件。它显示了一组多彩的盒子,供某种装饰用途使用。

我还有一个不相关的状态,即用户的姓名。

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import React from 'react';
import Boxes from './Boxes';

function App() {
const [name, setName] = React.useState('');
const [boxWidth, setBoxWidth] = React.useState(1);

const id = React.useId();

// 尝试改变其中一些值!
const boxes = [
{
flex: boxWidth,
background: 'hsl(345deg 100% 50%)',
},
{
flex: 3,
background: 'hsl(260deg 100% 40%)',
},
{
flex: 1,
background: 'hsl(50deg 100% 60%)',
},
];

return (
<>
<Boxes boxes={boxes} />

<section>
<div className="row">
<label htmlFor={`${id}-name`}>
Name:
</label>
<input
id={`${id}-name`}
type="text"
value={name}
onChange={(event) => {
setName(event.target.value);
}}
/>
</div>
<label htmlFor={`${id}-box-width`}>
First box width:
</label>
<input
id={`${id}-box-width`}
type="range"
min={1}
max={5}
step={0.01}
value={boxWidth}
onChange={(event) => {
setBoxWidth(Number(event.target.value));
}}
/>
</section>
</>
);
}

export default App;

Boxes 是一个纯组件,因为它在 Boxes.js 中用 React.memo() 包装的默认导出。这意味着它应该仅在其 props 发生变化时重新渲染。

然而,每当用户更改他们的名字时,Boxes 也会重新渲染!

这里有一个图表显示了这种动态。尝试在文本输入框中输入内容,注意两个组件是如何重新渲染的:
777.png
这是怎么回事?为什么我们的 React.memo() 在这里没有保护我们?

Boxes 组件只有一个 prop:boxes,并且看起来我们在每次渲染时都给它提供了相同的数据。它总是相同的:一个红色的盒子,一个宽宽的紫色盒子,一个黄色的盒子。我们确实有一个影响 boxes 数组的 boxWidth 状态变量,但我们并没有改变它!

问题在于:每次 React 重新渲染时,我们都在生成一个全新的数组。它们在值上是等价的,但在引用上却不是。

我认为如果我们先忘掉 React,聊聊普通的 JavaScript 会有帮助。让我们看一个类似的情况:

1
2
3
4
5
6
function getNumbers() {
return [1, 2, 3];
}
const firstResult = getNumbers();
const secondResult = getNumbers();
console.log(firstResult === secondResult);

你觉得呢?firstResult 是否等于 secondResult

从某种意义上说,它们是相等的。两个变量持有相同的结构 [1, 2, 3]。但这并不是 === 运算符实际上在检查的内容。

相反,=== 正在检查两个表达式是否是同一个东西。

我们创建了两个不同的数组。它们可能包含相同的内容,但它们不是同一个数组,就像两个相同的双胞胎不是同一个人一样。

每次我们调用 getNumbers 函数时,我们都会创建一个全新的数组,计算机内存中持有的独特事物。如果我们多次调用它,我们将在内存中存储多个该数组的副本。

注意,简单数据类型(如字符串、数字和布尔值)可以通过值进行比较。但对于数组和对象,它们只能通过引用进行比较。有关这种区别的更多信息,请查看 Dave Ceddia 的精彩博文:JavaScript 中引用的视觉指南

回到 React: 我们的 Boxes React 组件也是一个 JavaScript 函数。当我们渲染它时,我们调用该函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 每次我们渲染这个组件时,我们调用这个函数...
function App() {
// ...并生成一个全新的数组...
const boxes = [
{ flex: boxWidth, background: 'hsl(345deg 100% 50%)' },
{ flex: 3, background: 'hsl(260deg 100% 40%)' },
{ flex: 1, background: 'hsl(50deg 100% 60%)' },
];
// ...然后将其作为 prop 传递给该组件!
return (
<Boxes boxes={boxes} />
);
}

name 状态改变时,我们的 App 组件重新渲染,这会重新运行所有代码。我们构建了一个全新的 boxes 数组,并将其传递给 Boxes 组件。

Boxes 会重新渲染,因为我们给了它一个全新的数组!

盒子数组的结构在渲染之间没有改变,但这并不相关。React 所知道的就是 boxes prop 收到了一个新创建的、前所未见的数组。

为了解决这个问题,我们可以使用 useMemo 钩子:

1
2
3
4
5
6
7
const boxes = React.useMemo(() => {
return [
{ flex: boxWidth, background: 'hsl(345deg 100% 50%)' },
{ flex: 3, background: 'hsl(260deg 100% 40%)' },
{ flex: 1, background: 'hsl(50deg 100% 60%)' },
];
}, [boxWidth]);

与之前看到的素数示例不同,我们在这里并不担心计算开销。我们的唯一目标是保留对特定数组的引用。

我们将 boxWidth 列为依赖,因为我们希望 Boxes 组件在用户调整红色盒子的宽度时重新渲染。

我认为一个快速的草图将有助于说明。在之前的情况下,我们每次都会创建一个全新的数组,作为每个快照的一部分:

然而,通过 useMemo,我们可以重用先前创建的 boxes 数组:

通过在多个渲染中保留相同的引用,我们允许纯组件按我们希望的方式运行,忽略不影响用户界面的渲染。

useCallback 钩子

好吧,那么我们已经涵盖了 useMemo……那 useCallback 呢?

简而言之:它与 useMemo 完全相同,但用于函数而不是数组/对象。

与数组和对象类似,函数也是通过引用进行比较,而不是通过值:

1
2
3
4
5
6
7
const functionOne = function() {
return 5;
};
const functionTwo = function() {
return 5;
};
console.log(functionOne === functionTwo); // false

这意味着,如果我们在组件内部定义一个函数,它将在每次渲染时重新生成,产生一个相同但唯一的函数。

让我们看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React from 'react';
import MegaBoost from './MegaBoost';

function App() {
const [count, setCount] = React.useState(0);

function handleMegaBoost() {
setCount((currentValue) => currentValue + 1234);
}

return (
<>
Count: {count}
<button onClick={() => setCount(count + 1)}>
Click me!
</button>
<MegaBoost handleClick={handleMegaBoost} />
</>
);
}

export default App;

这个沙盒描绘了一个典型的计数器应用程序,但有一个特殊的“Mega Boost”按钮。这个按钮可以大幅增加计数,以防你赶时间,不想多次点击标准按钮。

MegaBoost 组件是一个纯组件,得益于 React.memo。它不依赖于 count……但每当 count 改变时,它都会重新渲染!

和我们看到的 boxes 数组一样,这里问题在于我们在每次渲染时都在生成一个全新的函数。如果我们渲染 3 次,我们将创建 3 个独立的 handleMegaBoost 函数,突破 React.memo 的保护。

使用我们关于 useMemo 学到的知识,我们可以这样解决问题:

1
2
3
4
5
const handleMegaBoost = React.useMemo(() => {
return function() {
setCount((currentValue) => currentValue + 1234);
}
}, []);

我们不是返回一个数组,而是返回一个函数。这个函数然后被存储在 handleMegaBoost 变量中。

这样可以工作……但还有更好的方法:

1
2
3
const handleMegaBoost = React.useCallback(() => {
setCount((currentValue) => currentValue + 1234);
}, []);

useCallbackuseMemo 的作用相同,但专门为函数构建。我们直接将一个函数传递给它,它会将该函数进行记忆,以便在渲染之间进行传递。

换句话说,这两个表达式具有相同的效果:

1
2
3
4
// 这个:
React.useCallback(function helloWorld(){}, []);
// ...在功能上等同于这个:
React.useMemo(() => function helloWorld(){}, []);

useCallback 是语法糖。它纯粹是为了在尝试记忆回调函数时让我们的生活变得更加轻松。

何时使用这些钩子

好吧,我们已经看到 useMemouseCallback 如何允许我们在多个渲染中传递引用,以重用复杂的计算或避免打破纯组件。问题是:我们应该多频繁使用它?

在我个人看来,把每一个对象/数组/函数都包裹在这些钩子中是浪费时间。大多数情况下,收益微不足道;React 的优化非常好,重新渲染往往并没有我们想象的那么慢或昂贵!

使用这些钩子的最佳方法是针对具体问题。如果你注意到你的应用变得有点迟缓,可以使用 React Profiler 来查找缓慢的渲染。在某些情况下,你能够通过重构应用来提高性能。在其他情况下,useMemouseCallback 可以帮助加速。

(如果你不确定如何使用 React Profiler,我在最近的博客文章《为什么 React 会重新渲染》中进行了详细介绍!)

尽管如此,有几个场景我确实会预先应用这些钩子。

这可能会在未来发生变化!
React 团队正在积极调查在编译阶段是否可能“自动记忆”代码。这仍处于研究阶段,但早期实验结果似乎很有希望。
在未来,所有这些事情都可能会自动为我们完成。不过在那之前,我们仍然需要自己进行优化。
有关更多信息,请查看 Xuan Huang 的讲座《没有记忆的 React》

通用自定义钩子内部

我最喜欢的小自定义钩子之一是 useToggle,这是一个友好的助手,几乎与 useState 完全相同,但只能在 truefalse 之间切换状态变量:

1
2
3
4
5
6
7
8
function App() {
const [isDarkMode, toggleDarkMode] = useToggle(false);
return (
<button onClick={toggleDarkMode}>
Toggle color theme
</button>
);
}

这是如何定义这个自定义钩子的:

1
2
3
4
5
6
7
function useToggle(initialValue) {
const [value, setValue] = React.useState(initialValue);
const toggle = React.useCallback(() => {
setValue(v => !v);
}, []);
return [value, toggle];
}

注意 toggle 函数是用 useCallback 进行记忆的。

当我构建像这样的自定义可重用钩子时,我喜欢尽可能使它们高效,因为我不知道它们将来会在哪里被使用。虽然在 95% 的情况下可能是过度设计,但如果我使用这个钩子 30 或 40 次,这很可能会帮助提高我的应用性能。

在上下文提供者内部

当我们使用上下文在应用程序中共享数据时,通常会将一个大对象作为 value 属性传递。

通常,记忆化这个对象是个好主意:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const AuthContext = React.createContext({});
function AuthProvider({ user, status, forgotPwLink, children }) {
const memoizedValue = React.useMemo(() => {
return {
user,
status,
forgotPwLink,
};
}, [user, status, forgotPwLink]);

return (
<AuthContext.Provider value={memoizedValue}>
{children}
</AuthContext.Provider>
);
}

**为什么这样做是有益的?:**可能有很多纯组件会消费这个上下文。如果不使用 useMemo,那么当 AuthProvider 的父组件重新渲染时,所有这些组件都将被迫重新渲染。

React 的乐趣

呼!你终于到了最后。我知道这个教程涉及了一些相当复杂的内容。😅

我知道这两个钩子很棘手,React 本身可能会让人感到非常压倒和困惑。这是一个困难的工具!

但问题是:如果你能克服最初的障碍,使用 React 将是一种绝对的乐趣。

我从 2015 年开始使用 React,它已成为我构建复杂用户界面和 web 应用程序的绝对最爱。我尝试过几乎所有的 JS 框架,但我在使用它们时的生产力不如使用 React 时高。