【问题标题】:Trying to use cleanup function in useEffect hook to cleanup img.onload尝试在 useEffect 挂钩中使用清理功能来清理 img.onload
【发布时间】:2020-03-05 11:29:39
【问题描述】:

我最近构建了一个 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 的异步性质。

【问题讨论】:

    标签: javascript reactjs react-hooks


    【解决方案1】:

    您正在设置不再安装的组件的状态。您可以使用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”钩子,它在初始化时将其设置为已安装,并在卸载时将其设置为未安装。

    【讨论】:

      【解决方案2】:

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

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

      【讨论】:

        【解决方案3】:

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

        你不需要另一个 useRefuseIfMounted 钩子——你只需要一个局部变量来跟踪效果是否仍然处于活动状态,并且你的效果应该返回一个清理函数,它将这个变量设置为 @ 987654323@.

        此外,您的效果应该取决于[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>
          );
        }
        

        【讨论】:

          【解决方案4】:

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

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

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2021-12-24
            • 2020-05-05
            • 1970-01-01
            • 2020-07-20
            • 2020-02-16
            • 2021-02-07
            • 1970-01-01
            • 2021-04-09
            相关资源
            最近更新 更多