尝试在useEffect挂钩中使用清理功能来清理img.onload

时间:2019-11-08 20:12:41

标签: javascript reactjs react-hooks

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

Search.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的异步性质。

4 个答案:

答案 0 :(得分:2)

您正在设置不再安装的组件的状态。您可以使用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对象,其.current属性已初始化为传递的参数(initialValue)。返回的对象将在组件的整个生命周期内保持不变。

这意味着您可以使用它创建对HTML元素的引用,但也可以在该引用中放置其他变量,例如布尔值。对于我的“ useIsMounted”挂钩,它将在初始化时将其设置为已安装,而在卸载时将其设置为已卸载。

答案 1 :(得分:1)

您将不再在树中设置组件的状态。我有一个小钩子实用程序可以帮助解决这种情况:

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))
    }

答案 2 :(得分:1)

尽管此问题已经有两个可行的答案,但我想给出第三个(希望更简单)的答案:

您不需要另一个useRefuseIfMounted钩子-您只需要一个局部变量即可跟踪效果是否仍处于活动状态,并且效果应返回一个clenup函数,该函数可以设置此变量为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>
  );
}

答案 3 :(得分:0)

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

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])