vlambda博客
学习文章列表

记录升级 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 APIStrictMode包装器中渲染我们的应用。

 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的指导。