记录升级 React 18 后发现的一些问题,很有用
最近你升级了 React 18
了吗?说说一些我的体验。我刚刚完成了React 18的升级,在进行了一些QA测试后,并没有发现任何问题。
不幸的是,接下来,收到一些来自其他开发者的内部bug报告,这些报告让我觉得useDebounce
这个 hook 工作得不太好。
我在下面的代码中创建了一个示例:我希望它在等待一秒钟后抛出一个“警报”对话框,但奇怪的是,这个对话框根本就没有运行。
1<html>
2 <head>
3 <meta charset="UTF-8">
4 <title>React Pad</title>
5 <script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
6 <script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>
7 <script src="https://unpkg.com/@babel/[email protected]/babel.min.js"></script>
8 <script src="https://unpkg.com/[email protected]/lodash.js"></script>
9 </head>
10 <body>
11 <div id="root"/>
12 <script>
13function useIsMounted() {
14 const isMountedRef = React.useRef(true);
15 React.useEffect(() => {
16 // isMountedRef.current = true;
17 return () => {
18 isMountedRef.current = false;
19 };
20 }, []);
21 return () => isMountedRef.current;
22}
23
24function useDebounce(cb, delay) {
25 const inputsRef = React.useRef({ cb, delay });
26 const isMounted = useIsMounted();
27 React.useEffect(() => {
28 inputsRef.current = { cb, delay };
29 });
30 return React.useCallback(
31 _.debounce((...args) => {
32 if (inputsRef.current.delay === delay && isMounted())
33 inputsRef.current.cb(...args);
34 }, delay),
35 [delay]
36 );
37}
38
39const App = () => {
40 const [val, setVal] = React.useState(0);
41
42 const say = useDebounce(() => {
43 alert(`Testing ${val}`);
44 }, 1000);
45
46 const UpdateValAndSay = () => {
47 setVal((v) => v + 1);
48 say();
49 };
50
51 return <button onClick={UpdateValAndSay}>Press Me</button>;
52}
53
54const StrictMode = React.StrictMode;
55
56ReactDOM.createRoot(document.getElementById('root')).render(
57 <StrictMode>
58 <App />
59 </StrictMode>
60);
61 </script>
62 </body>
63</html>
64
这很奇怪,因为它上周刚刚在我的机器上工作!为什么会这样呢?改变了什么?
先说原因吧:
我的应用程序在React 18中崩溃的原因是我使用的是StrictMode
。
只需进入index.js(或index.ts)文件,并更改这段代码:
1render(
2 <StrictMode>
3 <App />
4 </StrictMode>
5);
改成:
1render(
2 <App />
3);
4
现在所有在React 18中出现的bug都突然消失了。
只有一个问题:这些错误是真实存在的,并且在React 18之前就存在于代码库中——只是我没有意识到而已。
查找组件被损坏的证据
回头看看上面的例子,在第56 - 60行,我们使用了React 18的createRoot API
在StrictMode
包装器中渲染我们的应用。
1<html>
2 <head>
3 <meta charset="UTF-8">
4 <title>React Pad</title>
5 <script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
6 <script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>
7 <script src="https://unpkg.com/@babel/[email protected]/babel.min.js"></script>
8 <script src="https://unpkg.com/[email protected]/lodash.js"></script>
9 </head>
10 <body>
11 <div id="root"/>
12 <script>
13function useIsMounted() {
14 const isMountedRef = React.useRef(true);
15 React.useEffect(() => {
16 // isMountedRef.current = true;
17 return () => {
18 isMountedRef.current = false;
19 };
20 }, []);
21 return () => isMountedRef.current;
22}
23
24function useDebounce(cb, delay) {
25 const inputsRef = React.useRef({ cb, delay });
26 const isMounted = useIsMounted();
27 React.useEffect(() => {
28 inputsRef.current = { cb, delay };
29 });
30 return React.useCallback(
31 _.debounce((...args) => {
32 if (inputsRef.current.delay === delay && isMounted())
33 inputsRef.current.cb(...args);
34 }, delay),
35 [delay]
36 );
37}
38
39const App = () => {
40 const [val, setVal] = React.useState(0);
41
42 const say = useDebounce(() => {
43 alert(`Testing ${val}`);
44 }, 1000);
45
46 const UpdateValAndSay = () => {
47 setVal((v) => v + 1);
48 say();
49 };
50
51 return <button onClick={UpdateValAndSay}>Press Me</button>;
52}
53
54const StrictMode = React.StrictMode;
55
56ReactDOM.createRoot(document.getElementById('root')).render(
57 <StrictMode>
58 <App />
59 </StrictMode>
60);
61 </script>
62 </body>
63</html>
目前,当按下按钮时,它什么都不会做。但是,如果删除
StrictMode
和重新加载页面后,可以在一秒钟后看到一个警告。
查看代码,让我们添加一些控制台。登录到我们的useDebounce
,因为那是我们的函数应该被调用的地方。
1function useDebounce(cb, delay) {
2 const inputsRef = React.useRef({ cb, delay });
3 const isMounted = useIsMounted();
4 React.useEffect(() => {
5 inputsRef.current = { cb, delay };
6 });
7 return React.useCallback(
8 _.debounce((...args) => {
9 console.log("Before function is called", {inputsRef, delay, isMounted: isMounted()});
10 if (inputsRef.current.delay === delay && isMounted())
11 console.log("After function is called");
12 inputsRef.current.cb(...args);
13 }, delay),
14 [delay]
15 );
16}
哦!看起来isMounted
从来没有被设置为true
,因此inputsRef
。当前的回调函数没有被调用:这就是我们想要被取消的函数。
让我们来看看useIsMounted()
的代码库:
1function useIsMounted() {
2 const isMountedRef = React.useRef(true);
3 React.useEffect(() => {
4 return () => {
5 isMountedRef.current = false;
6 };
7 }, []);
8 return () => isMountedRef.current;
9}
乍一看,这段代码是有意义的。毕竟,当我们在useEffect
的返回函数中进行清理以在第一次渲染时移除它时,useRef
的初始setter在每次渲染开始时运行,对吗?
嗯,不完全是。
React 18 有什么改变
在旧版本的React中,你只需要装载一个组件,然后就可以了。因此,useRef和useState的初始值几乎可以被视为只设置了一次,然后就忘记了。
在React 18中,React开发团队决定改变这种行为,并在严格模式下重新挂载每个组件不止一次。这在很大程度上是因为未来React的一个潜在特性将具有这种行为。
你看,React团队希望在未来的版本中添加的一个特性利用了“可重用状态”的概念。可重用状态背后的基本思想是,如果你有一个标签被卸载(比如当用户标签离开时),然后重新安装(当用户标签返回时),React将恢复分配给该标签组件的数据。该数据立即可用,因此可以毫不犹豫地立即呈现相应的组件。
因此,虽然可以持久化useState中的数据,但必须正确清理和正确处理这些效果。引用React文档:
这个特性将为React提供更好的开箱即用性能,但需要组件对多次 mounted 和 destroyed 的效果有弹性。
然而,这种在React 18中严格模式下的行为转变不仅仅是为了保护React团队的未来:它还提醒你要正确地遵守React的规则,并按照预期清理你的行为。
毕竟,React团队自己已经警告过,一个空的依赖数组([]作为第二个参数)不应该保证它在很长一段时间内只运行一次。
事实上,这篇文章可能有点用词不当——React团队表示,他们已经在Facebook的核心代码库中升级了数千个组件,而没有出现重大问题。更有可能的是,大多数应用程序都能够毫无问题地升级到React的最新版本。
尽管如此,这些React的错误还是爬到了我们的应用程序中。虽然React团队可能没有预料到会有很多坏的应用,但这些错误似乎相当普遍,值得解释。
如何修复重新挂载的bug
我之前链接的代码是我在一个生产应用程序中写的,这是错误的。我们需要确保初始化在每个useEffect实例上运行,而不是依赖useRef来初始化该值一次。
1function useIsMounted() {
2 const isMountedRef = React.useRef(true);
3 React.useEffect(() => {
4 isMountedRef.current = true; // Added this line
5 return () => {
6 isMountedRef.current = false;
7 };
8 }, []);
9 return () => isMountedRef.current;
10}
反过来也是如此!我们需要确保对我们之前可能忘记的任何组件进行清理。
对于App和其他他们不想重新挂载的根元素,许多人会忽略这一规则,但对于新的严格模式行为,这种保证不再是安全的选择。
要在你的应用程序中解决这个应用程序,请寻找以下迹象:
有清理但没有设置的副作用(像我们的例子)
没有适当清理的副作用
利用useMemo和useEffect中的[]假设上述代码只运行一次
删除这段代码后,就可以回到一个功能完全的应用程序,并可以在应用程序中重新启用StrictMode !
总结
React 18带来了许多惊人的特性,比如新的suspense
特性、新的useId
钩子、自动批处理
等等。虽然重构工作时要支持这些特性有时可能令人沮丧,但重要的是要记住,它们为用户提供了体验上的升级。
例如,React 18还引入了一些功能来取消渲染,以便在需要处理快速用户输入时创造更好的体验。
有关React 18升级过程的更多信息,请点击查看关于如何升级到React 18的指导。