正在尝试使用useEffect钩子中的cleanup函数来清理img.onload



我最近构建了一个React组件(称为ItemIndexItem(,它在我的应用程序上显示图像。例如,我有一个搜索组件,它将显示筛选项目的索引。显示的每个Item都是一个ItemIndexItem组件。单击ItemIndexItem会将您发送到ItemShow页面,其中使用了相同的ItemIndexItem。

搜索.jsx

render() {
return (
<ul>
<li key={item.id}>
<div>
<Link to={`/items/${item.id}`}>
<ItemIndexItem src={item.photos[0].photoUrl} />
<p>${item.price}</p>
</Link>
</div>
</li>
...more li's
</ul>
)
}

ItemIndexItem.jsx

import React, { useState, useEffect } from "react";
export default function ItemIndexItem(props) {
const [imageIsReady, setImageIsReady] = useState(false);
useEffect(() => {
let img = new Image();
img.src = props.src;
img.onload = () => {
setImageIsReady(true);
};
});
if (!imageIsReady) return null;
return (
<div>
<img src={props.src} />
</div>
);
}

除了控制台中抛出的内存泄漏错误之外,该组件的工作完全符合要求:

无法对未安装的组件执行React状态更新。这是一个非操作,但它表明应用程序中存在内存泄漏。若要修复此问题,请取消useEffect清理函数中的所有订阅和异步任务。

在ItemIndexItem(由ItemShow创建(中

在div中(由ItemShow创建(

为了参考,这是ItemShow中的代码,我在其中渲染ItemIndexItem:

ItemShow.jsx

return (
...
<div>
<ul>
{this.props.item.photos.map(photo => {
return (
<li key={photo.photoUrl}>
<div>
<ItemIndexItem type='show' src={photo.photoUrl} />
</div>
</li>
);
})}
</ul>
</div>
...

我尝试使用useEffect返回函数将img设置为null:

return () => img = null;

然而,这并没有起到任何作用。由于我没有创建订阅,因此没有可删除的订阅。所以我认为问题出在.onload的异步性质上。

您正在设置不再安装的组件的状态。您可以使用useRef挂钩来确定您的组件是否仍在安装,例如:

function useIsMounted() {
const isMounted = React.useRef(true);
React.useEffect(() => {
return () => {
isMounted.current = false;
};
}, []);
return isMounted;
}

在你的ItemIndexItem。。。

export default function ItemIndexItem(props) {
const isMounted = useIsMounted();
const [imageIsReady, setImageIsReady] = useState(false);
...
img.onload = () => {
if (isMounted.current) {
setImageIsReady(true);
}
...
}

如useRef的React文档中所述。

useRef返回一个可变ref对象,其当前属性初始化为传递的参数(initialValue(。返回的对象将在组件的整个生存期内持续存在。

这意味着您可以使用它来创建对HTML元素的引用,但也可以在该引用中放置其他变量,例如布尔值。在我的"useIsMounted"钩子的情况下,它在初始化时将其设置为已装入,在卸载时将其设为未装入。

您正在设置一个不再在树中的组件的状态。我有一个小钩子实用程序来帮助处理这个场景:

import { useCallback, useEffect, useRef } from 'react'
export const useIfMounted = () => {
const isMounted = useRef(true)
useEffect(
() => () => {
isMounted.current = false
}, [])
const ifMounted = useCallback(
func => {
if (isMounted.current && func) {
func()
} else {
console.log('not mounted, not doing anything')
}
},[])
return ifMounted
}
export default useIfMounted

然后你可以这样使用:

const ifMounted = useIfMounted()
//other code
img.onload = () => {
ifMounted(() => setImageIsReady(true))
}

虽然这个问题已经有两个有效的答案,但我想给出第三个(希望更简单(答案:

你不需要另一个useRefuseIfMounted挂钩——你只需要一个局部变量来跟踪效果是否仍然有效,并且你的效果应该返回一个清除函数,该函数将该变量设置为false

此外,你的效果应该取决于[props.src],而不是[],因为如果props.src发生变化,你可能想等待新的图像:

import React, { useState, useEffect } from "react";
export default function ItemIndexItem(props) {
const [imageIsReady, setImageIsReady] = useState(false);
useEffect(() => {
if (imageIsReady) {
// Oh, oh, props.src changed ...
setImageIsReady(false);
}
let effectActive = true;
let img = new Image();
img.src = props.src;
img.onload = () => {
// Only call setImageIsReady if the effect is still active!
if (effectActive) {
setImageIsReady(true);
}
};
// The cleanup function below will be called,
// when either the ItemIndexItem component is
// unmounted or when props.src changes ...
return () => { effectActive = false; }
});
if (!imageIsReady) return null;
return (
<div>
<img src={props.src} />
</div>
);
}

类似的问题,我用这个解决了。

useEffect(() => {
let img = new Image()
//only continue if img is not null
if (img)
img.onload = () => {
setHeight(img.height)
setWidth(img.width)
img.src = src
}
}, [src])

最新更新