最近你升级了 React 18
了吗?说说一些我的体验。我刚刚完成了React 18的升级,在进行了一些QA测试后,并没有发现任何问题。
不幸的是,接下来,收到一些来自其他开发者的内部bug报告,这些报告让我觉得useDebounce
这个 hook 工作得不太好。
我在下面的代码中创建了一个示例:我希望它在等待一秒钟后抛出一个“警报”对话框,但奇怪的是,这个对话框根本就没有运行。
<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)文件,并更改这段代码:
render(
<StrictMode>
<App />
</StrictMode>
);
改成:
render(
<App />
);
现在所有在React 18中出现的bug都突然消失了。
只有一个问题:这些错误是真实存在的,并且在React 18之前就存在于代码库中——只是我没有意识到而已。
回头看看上面的例子,在第56 - 60行,我们使用了React 18的createRoot API
在StrictMode
包装器中渲染我们的应用。
<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
,因为那是我们的函数应该被调用的地方。
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()
的代码库:
function useIsMounted() {
const isMountedRef = React.useRef(true);
React.useEffect(() => {
return () => {
isMountedRef.current = false;
};
}, []);
return () => isMountedRef.current;
}
乍一看,这段代码是有意义的。毕竟,当我们在useEffect
的返回函数中进行清理以在第一次渲染时移除它时,useRef
的初始setter在每次渲染开始时运行,对吗?
嗯,不完全是。
在旧版本的React中,你只需要装载一个组件,然后就可以了。因此,useRef和useState的初始值几乎可以被视为只设置了一次,然后就忘记了。
在React 18中,React开发团队决定改变这种行为,并在严格模式下重新挂载每个组件不止一次。这在很大程度上是因为未来React的一个潜在特性将具有这种行为。
你看,React团队希望在未来的版本中添加的一个特性利用了“可重用状态”的概念。可重用状态背后的基本思想是,如果你有一个标签被卸载(比如当用户标签离开时),然后重新安装(当用户标签返回时),React将恢复分配给该标签组件的数据。该数据立即可用,因此可以毫不犹豫地立即呈现相应的组件。
因此,虽然可以持久化useState中的数据,但必须正确清理和正确处理这些效果。引用React文档:
这个特性将为React提供更好的开箱即用性能,但需要组件对多次 mounted 和 destroyed 的效果有弹性。
然而,这种在React 18中严格模式下的行为转变不仅仅是为了保护React团队的未来:它还提醒你要正确地遵守React的规则,并按照预期清理你的行为。
毕竟,React团队自己已经警告过,一个空的依赖数组([]作为第二个参数)不应该保证它在很长一段时间内只运行一次。
事实上,这篇文章可能有点用词不当——React团队表示,他们已经在Facebook的核心代码库中升级了数千个组件,而没有出现重大问题。更有可能的是,大多数应用程序都能够毫无问题地升级到React的最新版本。
尽管如此,这些React的错误还是爬到了我们的应用程序中。虽然React团队可能没有预料到会有很多坏的应用,但这些错误似乎相当普遍,值得解释。
我之前链接的代码是我在一个生产应用程序中写的,这是错误的。我们需要确保初始化在每个useEffect实例上运行,而不是依赖useRef来初始化该值一次。
function useIsMounted() {
const isMountedRef = React.useRef(true);
React.useEffect(() => {
isMountedRef.current = true; // Added this line
return () => {
isMountedRef.current = false;
};
}, []);
return () => isMountedRef.current;
}
反过来也是如此!我们需要确保对我们之前可能忘记的任何组件进行清理。
对于App和其他他们不想重新挂载的根元素,许多人会忽略这一规则,但对于新的严格模式行为,这种保证不再是安全的选择。
要在你的应用程序中解决这个应用程序,请寻找以下迹象:
React 18带来了许多惊人的特性,比如新的suspense
特性、新的useId
钩子、自动批处理
等等。虽然重构工作时要支持这些特性有时可能令人沮丧,但重要的是要记住,它们为用户提供了体验上的升级。
例如,React 18还引入了一些功能来取消渲染,以便在需要处理快速用户输入时创造更好的体验。
有关React 18升级过程的更多信息,请点击查看关于如何升级到React 18的指导。