清除 React Hooks 中未安装组件上的内存泄漏
Posted
技术标签:
【中文标题】清除 React Hooks 中未安装组件上的内存泄漏【英文标题】:Cleanup memory leaks on an Unmounted Component in React Hooks 【发布时间】:2020-05-03 22:09:35 【问题描述】:我是使用 React 的新手,所以这可能很容易实现,但即使我做了一些研究,我也无法自己弄清楚。如果这太愚蠢了,请原谅我。
上下文
我将 Inertia.js 与 Laravel(后端)和 React(前端)适配器一起使用。如果您不了解 Inertia,则基本上是:
Inertia.js 可让您快速构建现代单页 React、Vue 和 使用经典服务器端路由和控制器的 Svelte 应用。
问题
我正在做一个简单的登录页面,该页面有一个表单,提交后将执行 POST 请求以加载下一页。它似乎工作正常,但在其他页面中控制台显示以下警告:
警告:无法对未安装的组件执行 React 状态更新。 这是一个空操作,但它表明您的应用程序中存在内存泄漏。 要修复,请取消 useEffect 中的所有订阅和异步任务 清理功能。
在登录中(由 Inertia 创建)
相关代码(我已将其简化以避免不相关的行):
import React, useEffect, useState from 'react'
import Layout from "../../Layouts/Auth";
/** other imports */
const login = (props) =>
const errors = usePage();
const [values, setValues] = useState(email: '', password: '',);
const [loading, setLoading] = useState(false);
function handleSubmit(e)
e.preventDefault();
setLoading(true);
Inertia.post(window.route('login.attempt'), values)
.then(() =>
setLoading(false); // Warning : memory leaks during the state update on the unmounted component <--------
)
return (
<Layout title="Access to the system">
<div>
<form action=handleSubmit>
/*the login form*/
<button type="submit">Access</button>
</form>
</div>
</Layout>
);
;
export default login;
现在,我知道我必须执行清理功能,因为请求的承诺是生成此警告的原因。我知道我应该使用useEffect
,但我不知道在这种情况下如何应用它。我见过一个值改变的例子,但是如何在这种调用中做到这一点?
提前致谢。
更新
根据要求,该组件的完整代码:
import React, useState from 'react'
import Layout from "../../Layouts/Auth";
import usePage from '@inertiajs/inertia-react'
import Inertia from "@inertiajs/inertia";
import LoadingButton from "../../Shared/LoadingButton";
const login = (props) =>
const errors = usePage();
const [values, setValues] = useState(email: '', password: '',);
const [loading, setLoading] = useState(false);
function handleChange(e)
const key = e.target.id;
const value = e.target.value;
setValues(values => (
...values,
[key]: value,
))
function handleSubmit(e)
e.preventDefault();
setLoading(true);
Inertia.post(window.route('login.attempt'), values)
.then(() =>
setLoading(false);
)
return (
<Layout title="Inicia sesión">
<div className="w-full flex items-center justify-center">
<div className="w-full max-w-5xl flex justify-center items-start z-10 font-sans text-sm">
<div className="w-2/3 text-white mt-6 mr-16">
<div className="h-16 mb-2 flex items-center">
<span className="uppercase font-bold ml-3 text-lg hidden xl:block">
Optima spark
</span>
</div>
<h1 className="text-5xl leading-tight pb-4">
Vuelve inteligente tus operaciones
</h1>
<p className="text-lg">
Recoge data de tus instalaciones de forma automatizada; accede a información histórica y en tiempo real
para que puedas analizar y tomar mejores decisiones para tu negocio.
</p>
<button type="submit" className="bg-yellow-600 w-40 hover:bg-blue-dark text-white font-semibold py-2 px-4 rounded mt-8 shadow-md">
Más información
</button>
</div>
<div className="w-1/3 flex flex-col">
<div className="bg-white text-gray-700 shadow-md rounded rounded-lg px-8 pt-6 pb-8 mb-4 flex flex-col">
<div className="w-full rounded-lg h-16 flex items-center justify-center">
<span className="uppercase font-bold text-lg">Acceder</span>
</div>
<form onSubmit=handleSubmit className=`relative $loading ? 'invisible' : 'visible'`>
<div className="mb-4">
<label className="block text-gray-700 text-sm font-semibold mb-2" htmlFor="email">
Email
</label>
<input
id="email"
type="text"
className=" appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 outline-none focus:border-1 focus:border-yellow-500"
placeholder="Introduce tu e-mail.."
name="email"
value=values.email
onChange=handleChange
/>
errors.email && <p className="text-red-500 text-xs italic"> errors.email[0] </p>
</div>
<div className="mb-6">
<label className="block text-gray-700 text-sm font-semibold mb-2" htmlFor="password">
Contraseña
</label>
<input
className=" appearance-none border border-red rounded w-full py-2 px-3 text-gray-700 mb-3 outline-none focus:border-1 focus:border-yellow-500"
id="password"
name="password"
type="password"
placeholder="*********"
value=values.password
onChange=handleChange
/>
errors.password && <p className="text-red-500 text-xs italic"> errors.password[0] </p>
</div>
<div className="flex flex-col items-start justify-between">
<LoadingButton loading=loading label='Iniciar sesión' />
<a className="font-semibold text-sm text-blue hover:text-blue-700 mt-4"
href="#">
<u>Olvidé mi contraseña</u>
</a>
</div>
<div
className=`absolute top-0 left-0 right-0 bottom-0 flex items-center justify-center $!loading ? 'invisible' : 'visible'`
>
<div className="lds-ellipsis">
<div></div>
<div></div>
<div></div>
<div></div>
</div>
</div>
</form>
</div>
<div className="w-full flex justify-center">
<a href="https://optimaee.com">
</a>
</div>
</div>
</div>
</div>
</Layout>
);
;
export default login;
【问题讨论】:
@Sohail 我已经添加了组件的完整代码 您是否尝试简单地删除.then(() => )
?
【参考方案1】:
因为它是异步承诺调用,所以您必须使用可变引用变量(使用 useRef) 来检查已经卸载的组件以进行异步响应的下一次处理(避免内存泄漏):
警告:无法对未安装的组件执行 React 状态更新。
在这种情况下你应该使用两个 React Hooks:useRef
和 useEffect
。
以useRef
为例,可变变量_isMounted
总是指向内存中的同一个引用(不是局部变量)
如果需要可变变量,useRef 是首选钩子。不同于本地 变量,React 确保在每个变量中返回相同的引用 使成为。如果你愿意,它与类组件中的 this.myVar
相同
例子:
const login = (props) =>
const _isMounted = useRef(true); // Initial value _isMounted = true
useEffect(() =>
return () => // ComponentWillUnmount in Class Component
_isMounted.current = false;
, []);
function handleSubmit(e)
e.preventDefault();
setLoading(true);
ajaxCall = Inertia.post(window.route('login.attempt'), values)
.then(() =>
if (_isMounted.current) // Check always mounted component
// continue treatment of AJAX response... ;
)
在同一场合,让我向您解释有关此处使用的 React Hooks 的更多信息。另外,我将比较函数式组件中的 React Hooks (React >16.8) 和类组件中的 LifeCycle。
useEffect :大多数副作用发生在钩子内部。副作用的示例包括:数据获取、设置订阅和手动更改 DOM React 组件。 useEffect 替换了 Class Component 中的很多 LifeCycles (componentDidMount, componentDidUpate, componentWillUnmount)
useEffect(fnc, [dependency1, dependency2, ...]); // dependencies array argument is optional
useEffect 的默认行为在第一次渲染后运行(如 ComponentDidMount),如果您没有依赖项,则在每次更新后渲染 (如 ComponentDidUpdate)。是这样的:useEffect(fnc);
为 useEffect 提供一系列依赖项将改变其生命周期。在这个例子中:useEffect 将在第一次渲染后调用一次,并且每次计数发生变化
导出默认函数 () const [count, setCount] = useState(0);
useEffect(fnc, [count]);
useEffect 将在第一次渲染后仅运行一次(如 ComponentDidMount),如果您为依赖项放置一个空数组。是这样的:useEffect(fnc, []);
为了防止资源泄漏,所有东西都必须在钩子的生命周期结束时进行处理(如 ComponentWillUnmount)。例如,使用空的依赖数组,返回的函数将在组件卸载后调用。是这样的:
useEffect(() => 返回 fnc_cleanUp; // fnc_cleanUp 将取消所有订阅和异步任务(例如:clearInterval) , []);
useRef :返回可变引用对象,其 .current 属性为 初始化为传递的参数(initialValue)。返回的对象 将在组件的整个生命周期内持续存在。
示例:对于上面的问题,我们不能在这里使用局部变量,因为它会丢失并在每次更新渲染时重新启动。
const login = (props) =>
let _isMounted= true; // it isn't good because of a local variable, so the variable will be lost and re-defined on every update render
useEffect(() =>
return () =>
_isMounted = false; // not good
, []);
// ...
因此,结合 useRef 和 useEffect,我们可以彻底清除内存泄漏。
您可以阅读更多关于 React Hooks 的好链接:
[EN]https://medium.com/@sdolidze/the-iceberg-of-react-hooks-af0b588f43fb
[FR]https://blog.soat.fr/2019/11/react-hooks-par-lexemple/
【讨论】:
这行得通。今天晚些时候,我将阅读提供的链接以实际了解这如何解决问题。如果您可以详细说明响应以包含详细信息,那就太好了,因此对其他人有帮助,并在宽限期后将赏金奖励给您。谢谢。 感谢您接受我的回答。我会考虑你的要求,明天再做。【参考方案2】:在改变状态之前,你应该先检查组件是否仍然挂载。
正如上面@SanjiMika 所说,当有一个导致此错误的异步操作时, 这意味着您在卸载组件后试图改变它的状态。
react-use
为此提供了钩子,你有两个选择:
选项#1:useMountedState
// check if isMounted before changing any state
const isMounted = useMountedState();
useEffect(() =>
const asyncAction = executeAsyncAction();
asyncAction.then(result =>
if (isMounted)
// It's safe to mutate state here
);
, []);
选项 #2:useUnmountPromise
/* `resolveWhileMounted` wraps your promise, and returns a promise that will resolve
* only while the component is still mounted */
const resolveWhileMounted = useUnmountPromise();
useEffect(async () =>
const asyncAction = executeAsyncAction();
resolveWhileMounted(asyncAction).then(result =>
// It's safe to mutate state here
);
, []);
【讨论】:
【参考方案3】:您可以使用Inertia
的'cancelActiveVisits' 方法取消useEffect
清理挂钩中的活动visit
。
因此,通过此调用,活动的visit
将被取消,状态将不会更新。
useEffect(() =>
return () =>
Inertia.cancelActiveVisits(); //To cancel the active visit.
, []);
如果Inertia
请求被取消,那么它将返回一个空响应,因此您必须添加一个额外的检查来处理空响应。
添加 add catch 块以处理任何潜在的错误。
function handleSubmit(e)
e.preventDefault();
setLoading(true);
Inertia.post(window.route('login.attempt'), values)
.then(data =>
if(data)
setLoading(false);
)
.catch( error =>
console.log(error);
);
替代方式(解决方法)
您可以使用useRef
来保存组件的状态,并在此基础上更新state
。
问题:
因为handleSubmit
正在尝试更新组件的状态,即使组件已从 dom 中卸载。
解决方案:
设置一个标志来保存component
的状态,如果component
是mounted
,那么flag
的值将是true
,如果component
是unmounted
,则标志值将是假的。所以基于此我们可以更新state
。
对于标志状态,我们可以使用useRef
来保存引用。
useRef
返回一个可变引用对象,其.current
属性被初始化为传递的参数(initialValue)。返回的对象将在组件的整个生命周期内持续存在。 在useEffect
中返回一个函数,该函数将设置组件的状态(如果已卸载)。
然后在useEffect
的清理函数中我们可以将标志设置为false.
使用Effecr清理功能
useEffect
挂钩允许使用清理功能。任何时候效果不再有效,例如当使用该效果的组件正在卸载时,调用此函数来清理所有内容。在我们的例子中,我们可以将标志设置为 false。
示例:
let _componentStatus.current = useRef(true);
useEffect(() =>
return () =>
_componentStatus.current = false;
, []);
在handleSubmit中,我们可以检查组件是否已挂载,并据此更新状态。
function handleSubmit(e)
e.preventDefault();
setLoading(true);
Inertia.post(window.route('login.attempt'), values)
.then(() =>
if (_componentStatus.current)
setLoading(false);
else
_componentStatus = null;
)
在 else 中将 _componentStatus
设置为 null 以避免任何内存泄漏。
【讨论】:
【参考方案4】:基于功能和基于类的 ??
// function based METHOD 1 ??
const unsubscribe = useRef();
useEffect(() =>
unsubscribe.current = setTimeout(() =>
// do something
, 1000);
return () =>
clearTimeout(unsubscribe.current);
unsubscribe.current = null;
;
, []);
// function based METHOD 2 ??
const [news, setNews] = useState();
const unsubscribe = useRef();
useEffect(() =>
unsubscribe.current = true
axios.get('domain').then((result) =>
if (unsubscribe)
setNews(result);
);
return () =>
unsubscribe.current = false;
;
, []);
// class based METHOD 1 ??
unsubscribe = null;
componentDidMount()
this.unsubscribe = setTimeout(() =>
// do something
, 1000);
componentWillUnmount()
clearTimeout(this.unsubscribe);
this.unsubscribe = null;
// class based METHOD 4
unsubscribe = false;
constructor(props)
super(props);
this.state =
news: [],
;
componentDidMount()
this.unsubscribe = true;
axios.get('domain').then((result) =>
if (this.unsubscribe)
this.setState(
news: result.data.hits,
);
);
componentWillUnmount()
this.unsubscribe = false;
【讨论】:
请添加更多详细信息以扩展您的答案,例如工作代码或文档引用。【参考方案5】:我知道我来晚了,但有一个更简单的解决方案。将您的代码设计为在卸载后不使用状态。
当组件被卸载并且您调用 setState
时会出现警告
.then(() =>
// Warning : memory leaks during the state update on the unmounted component <--------
setLoading(false);
)
你并不真正需要,因为如果组件已经卸载,就会发生这种情况。因此,如果您只是将其删除,您将不会收到警告。
所以我对现在阅读本文的任何人的建议是,在调用卸载组件的函数后尝试重构代码以不设置状态。
【讨论】:
以上是关于清除 React Hooks 中未安装组件上的内存泄漏的主要内容,如果未能解决你的问题,请参考以下文章
TypeError:无法读取 React-hooks 中未定义的属性“toLowerCase”
在 React Hooks useEffect cleanup 中取消 Axios REST 调用失败