为什么 React 18 会破坏你的应用
由 Mux 赞助的 DEV 全球展示挑战赛:展示你的项目!
你刚刚完成了React 18 的升级,经过一些简单的质量保证测试后,没有发现任何问题。“升级很简单,”你心想。
不幸的是,之后你收到了一些来自其他开发人员的内部错误报告,这些报告表明你的防抖钩子似乎工作不正常。你决定创建一个最小复现示例,并演示该钩子的功能。
你以为等待一秒钟后会弹出一个“警告”对话框,但奇怪的是,对话框根本没有运行。
这很奇怪,因为上周它在你的电脑上还运行正常!为什么会这样?发生了什么变化?
你的应用在 React 18 中崩溃的原因是你在使用StrictMode。
只需打开你的index.js(或index.ts)文件,并更改以下代码:
render(
<StrictMode>
<App />
</StrictMode>
);
阅读起来就是这样:
render(
<App />
);
React 18 中似乎引入到你的应用程序中的所有错误都突然消失了。
只有一个问题:这些错误是真实存在的,并且在 React 18 之前就存在于你的代码库中——你只是没有意识到而已。
部件损坏的证据
回顾我们之前的示例,我们在第 56 - 60 行中使用React 18 的createRootAPI来渲染包装器App内部的内容StrictMode。
目前,按下按钮没有任何反应。但是,如果移除
StrictMode重新加载页面Alert后,可以看到一秒钟的防抖效果。
查看代码,让我们console.log在代码中添加一些 s useDebounce,因为我们的函数应该在那里被调用。
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]
);
}
Before function is called Object { inputsRef: {…}, delay: 1000, isMounted: false }
哦!看来isMounted它从未被设置为 true,因此inputsRef.current回调函数没有被调用:而这正是我们想要进行防抖处理的函数。
我们来看一下useIsMounted()代码库:
function useIsMounted() {
const isMountedRef = React.useRef(true);
React.useEffect(() => {
return () => {
isMountedRef.current = false;
};
}, []);
return () => isMountedRef.current;
}
这段代码乍一看似乎很有道理。毕竟,虽然我们在返回函数中进行清理以useEffect在首次渲染时将其移除,useRef但的初始设置器会在每次渲染开始时运行,对吧?
嗯,也不完全是。
React 18 有哪些变化?
在旧版本的 React 中,组件只需挂载一次即可。因此,`is`useRef和 `is`的初始值useState几乎可以视为设置一次后就被遗忘了。
在 React 18 中,React 开发团队决定改变这种行为,在严格模式下,每个组件会被多次重新挂载。这主要是因为 React 未来某个潜在的功能将会采用这种行为。
React 团队希望在未来的版本中添加一项名为“可复用状态”的功能。可复用状态的基本思想是,如果一个标签页组件被卸载(例如用户切换到其他标签页时),然后又被重新挂载(例如用户切换到其他标签页时),React 会恢复分配给该组件的数据。由于这些数据可以立即使用,因此可以毫不犹豫地立即渲染相应的组件。
因此,虽然例如数据内部的数据useState可能会被持久化,但必须正确地清理和处理副作用。引用 React 文档:
此功能将使 React 具有更好的开箱即用性能,但要求组件能够承受多次挂载和销毁的效果。
然而,React 18 中严格模式下的这种行为转变不仅仅是 React 团队为了保护未来而采取的措施:它也是提醒人们要正确遵守 React 的规则,并按预期清理自己的操作。
毕竟,React 团队自己也早就警告过,空的依赖数组([]作为第二个参数)不能保证它只运行一次。
事实上,这篇文章的标题可能有些误导——React 团队表示,他们已经升级了 Facebook 核心代码库中的数千个组件,而且没有出现重大问题。很可能,绝大多数应用程序都能顺利升级到最新版本的 React。
尽管如此,这些 React 错误还是会悄悄地渗入我们的应用程序中。虽然 React 团队可能预计不会出现太多导致应用程序崩溃的情况,但这些错误似乎相当普遍,因此有必要做出解释。
如何修复重新挂载错误
我之前链接的代码是我在生产应用程序中编写的,它是错误的。useRef我们不能只依赖一次初始化,而需要确保初始化在每次实例化时都运行useEffect。
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
删除此代码后,您的应用程序应该可以完全正常运行,并且可以在应用程序中重新启用严格模式!
结论
React 18 带来了许多令人惊艳的新功能,例如新的 Suspense 特性、新的 useId hook、自动批处理等等。虽然为了支持这些功能而进行的重构工作有时可能会令人沮丧,但重要的是要记住,它们能为用户带来实实在在的好处。
例如,React 18 还引入了一些功能来防止渲染抖动,以便在需要处理快速用户输入时创造更好的体验。
有关 React 18 升级过程的更多信息,请参阅我们的 React 18 升级指南。
文章来源:https://dev.to/coderpad/why-react-18-broke-your-app-4730