前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >记录升级 React 18 后发现的一些问题,很有用

记录升级 React 18 后发现的一些问题,很有用

作者头像
前端修罗场
发布2022-07-29 08:09:59
1.2K0
发布2022-07-29 08:09:59
举报
文章被收录于专栏:Web 技术

最近你升级了 React 18 了吗?说说一些我的体验。我刚刚完成了React 18的升级,在进行了一些QA测试后,并没有发现任何问题。

不幸的是,接下来,收到一些来自其他开发者的内部bug报告,这些报告让我觉得useDebounce 这个 hook 工作得不太好。

我在下面的代码中创建了一个示例:我希望它在等待一秒钟后抛出一个“警报”对话框,但奇怪的是,这个对话框根本就没有运行。

代码语言:javascript
复制
<html>
  <head>
    <meta charset="UTF-8">
    <title>React Pad</title>
    <script src="https://unpkg.com/react@18.0.0-rc.0/umd/react.development.js"></script>
    <script src="https://unpkg.com/react-dom@18.0.0-rc.0/umd/react-dom.development.js"></script>
    <script src="https://unpkg.com/@babel/standalone@7.13.12/babel.min.js"></script>
    <script src="https://unpkg.com/lodash@4.17.21/lodash.js"></script>
  </head>
  <body>
    <div id="root"/>
    <script>
function useIsMounted() {
  const isMountedRef = React.useRef(true);
  React.useEffect(() => {
     // isMountedRef.current = true;
     return () => {
      isMountedRef.current = false;
    };
  }, []);
  return () => isMountedRef.current;
}

function useDebounce(cb, delay) {
  const inputsRef = React.useRef({ cb, delay });
  const isMounted = useIsMounted();
  React.useEffect(() => {
    inputsRef.current = { cb, delay };
  });
  return React.useCallback(
    _.debounce((...args) => {
      if (inputsRef.current.delay === delay && isMounted())
        inputsRef.current.cb(...args);
    }, delay),
    [delay]
  );
}

const App = () => {
   const [val, setVal] = React.useState(0);

  const say = useDebounce(() => {
    alert(`Testing ${val}`);
  }, 1000);

  const UpdateValAndSay = () => {
    setVal((v) => v + 1);
    say();
  };

  return <button onClick={UpdateValAndSay}>Press Me</button>;
}

const StrictMode = React.StrictMode;

ReactDOM.createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>
);
    </script>
  </body>
</html>

这很奇怪,因为它上周刚刚在我的机器上工作!为什么会这样呢?改变了什么?

先说原因吧:

我的应用程序在React 18中崩溃的原因是我使用的是StrictMode

只需进入index.js(或index.ts)文件,并更改这段代码:

代码语言:javascript
复制
render(
  <StrictMode>
    <App />
  </StrictMode>
);

改成:

代码语言:javascript
复制
render(
    <App />
);

现在所有在React 18中出现的bug都突然消失了。

只有一个问题:这些错误是真实存在的,并且在React 18之前就存在于代码库中——只是我没有意识到而已。

查找组件被损坏的证据

回头看看上面的例子,在第56 - 60行,我们使用了React 18的createRoot APIStrictMode包装器中渲染我们的应用。

代码语言:javascript
复制
<html>
  <head>
    <meta charset="UTF-8">
    <title>React Pad</title>
    <script src="https://unpkg.com/react@18.0.0-rc.0/umd/react.development.js"></script>
    <script src="https://unpkg.com/react-dom@18.0.0-rc.0/umd/react-dom.development.js"></script>
    <script src="https://unpkg.com/@babel/standalone@7.13.12/babel.min.js"></script>
    <script src="https://unpkg.com/lodash@4.17.21/lodash.js"></script>
  </head>
  <body>
    <div id="root"/>
    <script>
function useIsMounted() {
  const isMountedRef = React.useRef(true);
  React.useEffect(() => {
     // isMountedRef.current = true;
     return () => {
      isMountedRef.current = false;
    };
  }, []);
  return () => isMountedRef.current;
}

function useDebounce(cb, delay) {
  const inputsRef = React.useRef({ cb, delay });
  const isMounted = useIsMounted();
  React.useEffect(() => {
    inputsRef.current = { cb, delay };
  });
  return React.useCallback(
    _.debounce((...args) => {
      if (inputsRef.current.delay === delay && isMounted())
        inputsRef.current.cb(...args);
    }, delay),
    [delay]
  );
}

const App = () => {
   const [val, setVal] = React.useState(0);

  const say = useDebounce(() => {
    alert(`Testing ${val}`);
  }, 1000);

  const UpdateValAndSay = () => {
    setVal((v) => v + 1);
    say();
  };

  return <button onClick={UpdateValAndSay}>Press Me</button>;
}

const StrictMode = React.StrictMode;

ReactDOM.createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>
);
    </script>
  </body>
</html>

目前,当按下按钮时,它什么都不会做。但是,如果删除

StrictMode和重新加载页面后,可以在一秒钟后看到一个警告。

查看代码,让我们添加一些控制台。登录到我们的useDebounce,因为那是我们的函数应该被调用的地方。

代码语言:javascript
复制
function useDebounce(cb, delay) {
  const inputsRef = React.useRef({ cb, delay });
  const isMounted = useIsMounted();
  React.useEffect(() => {
    inputsRef.current = { cb, delay };
  });
  return React.useCallback(
    _.debounce((...args) => {
        console.log("Before function is called", {inputsRef, delay, isMounted: isMounted()});
          if (inputsRef.current.delay === delay && isMounted())
                      console.log("After function is called");
                  inputsRef.current.cb(...args);
        }, delay),
    [delay]
  );
}

哦!看起来isMounted从来没有被设置为true,因此inputsRef。当前的回调函数没有被调用:这就是我们想要被取消的函数。

让我们来看看useIsMounted()的代码库:

代码语言:javascript
复制
function useIsMounted() {
  const isMountedRef = React.useRef(true);
  React.useEffect(() => {
    return () => {
          isMountedRef.current = false;
    };
  }, []);
  return () => isMountedRef.current;
}

乍一看,这段代码是有意义的。毕竟,当我们在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来初始化该值一次。

代码语言:javascript
复制
function useIsMounted() {
  const isMountedRef = React.useRef(true);
  React.useEffect(() => {
  isMountedRef.current = true; // Added this line  
  return () => {
      isMountedRef.current = false;
    };
  }, []);
  return () => isMountedRef.current;
}

反过来也是如此!我们需要确保对我们之前可能忘记的任何组件进行清理

对于App和其他他们不想重新挂载的根元素,许多人会忽略这一规则,但对于新的严格模式行为,这种保证不再是安全的选择。

要在你的应用程序中解决这个应用程序,请寻找以下迹象:

  • 有清理但没有设置的副作用(像我们的例子)
  • 没有适当清理的副作用
  • 利用useMemo和useEffect中的[]假设上述代码只运行一次 删除这段代码后,就可以回到一个功能完全的应用程序,并可以在应用程序中重新启用StrictMode !

总结

React 18带来了许多惊人的特性,比如新的suspense特性、新的useId钩子、自动批处理等等。虽然重构工作时要支持这些特性有时可能令人沮丧,但重要的是要记住,它们为用户提供了体验上的升级。

例如,React 18还引入了一些功能来取消渲染,以便在需要处理快速用户输入时创造更好的体验。

有关React 18升级过程的更多信息,请点击查看关于如何升级到React 18的指导。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-04-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端修罗场 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 查找组件被损坏的证据
  • React 18 有什么改变
  • 如何修复重新挂载的bug
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档