在react-query onSuccess事件中更新react状态会导致内存泄漏

发布于 2025-01-10 10:59:18 字数 3332 浏览 0 评论 0原文

我有一个名为 ImageDisplay 的组件。该组件使用自定义反应钩子(useImageUpload)使用反应查询将图像上传到cloudinary。

自定义钩子 useImageUpload 包含一个将上传图像的 url 存储在数组中的状态。自定义挂钩返回一个对象,其中包含图像 urls 数组、上传新图像的函数和 isLoading 布尔值。

图片上传成功。但看起来,这个反应查询片段正在导致内存泄漏。

  onSuccess: ({ data }) => {
    console.log("Image uploaded successfully");
    setImages([...images, data.secure_url]);
  },

输入图片此处描述

如何更新 onSuccess 事件上的图像数组?

ImageDisplay.js

import React, { useEffect } from "react";
import "styles/ImageDisplay.scss";
import { Field } from "formik";
import Skeleton from "react-loading-skeleton";
import "react-loading-skeleton/dist/skeleton.css";
import useImageUpload from "utils/useImageUpload";

export default function ImageDisplay() {
  let { images, uploadImage, uploading } = useImageUpload();

  const handleFileChange = (e) => {
    const inputFile = e.target.files[0];
    uploadImage(inputFile);
  };
  useEffect(() => {
    console.log(images);
  }, [images]);

  return (
    <>
      <div className="image-display border mb-3 px-3">
        <div className="image-preview text-center">
          <h6>Upload product images to attract customers</h6>
          {/* <Skeleton width={300} height={300} /> */}
        </div>
        <hr />
        <div className="img-choose border p-2 ">
          <div className="options">
            <Skeleton width={100} height={100} />
            <Skeleton width={100} height={100} />
            <Skeleton width={100} height={100} />
            <Skeleton width={100} height={100} />
            <Skeleton width={100} height={100} />
            <Skeleton width={100} height={100} />
          </div>
        </div>
        .
      </div>
      <div className="img-input">
        <Field
          type="file"
          name="productImages"
          id="productImages"
          onChange={handleFileChange}
          accept="image/*"
        />
        <h5>Select product images to upload</h5>
      </div>
    </>
  );
}

useImageUpload.js

import axios from "axios";
import { useState } from "react";
import { useMutation } from "react-query";

const useImageUpload = () => {
  const [images, setImages] = useState([]);

  let { isLoading: uploading, mutateAsync: uploadImage } = useMutation(
    async (data) => {
      let formData = new FormData();
      formData.append("file", data);
      formData.append("upload_preset", "uploadPresetHere");
      formData.append("cloud_name", "nameHere");

      return await axios.post(
        "cloudinary_url",
        formData,
        {
          withCredentials: false,
        }
      );
    },
    {
      onSuccess: ({ data }) => {
        console.log("Image uploaded successfully");
        setImages([...images, data.secure_url]);
      },
      onError: (error) => {
        console.log(error.response);
      },
    }
  );

  return { images, uploadImage, uploading };
};

export default useImageUpload;

抱歉,如果我在问题中犯了任何错误。

I have component called ImageDisplay. And that component uses a custom react hook (useImageUpload) to upload images to cloudinary using react-query.

The custom hook useImageUpload contains a state which stores the urls of uploaded images in an array. The custom hook returns an object which contains images urls array, a function to upload new images, and isLoading boolean.

The image is being uploaded successfully. But looks like, this react-query snippet is causing memory leak.

  onSuccess: ({ data }) => {
    console.log("Image uploaded successfully");
    setImages([...images, data.secure_url]);
  },

enter image description here

How can I update the images array on onSuccess event?

ImageDisplay.js

import React, { useEffect } from "react";
import "styles/ImageDisplay.scss";
import { Field } from "formik";
import Skeleton from "react-loading-skeleton";
import "react-loading-skeleton/dist/skeleton.css";
import useImageUpload from "utils/useImageUpload";

export default function ImageDisplay() {
  let { images, uploadImage, uploading } = useImageUpload();

  const handleFileChange = (e) => {
    const inputFile = e.target.files[0];
    uploadImage(inputFile);
  };
  useEffect(() => {
    console.log(images);
  }, [images]);

  return (
    <>
      <div className="image-display border mb-3 px-3">
        <div className="image-preview text-center">
          <h6>Upload product images to attract customers</h6>
          {/* <Skeleton width={300} height={300} /> */}
        </div>
        <hr />
        <div className="img-choose border p-2 ">
          <div className="options">
            <Skeleton width={100} height={100} />
            <Skeleton width={100} height={100} />
            <Skeleton width={100} height={100} />
            <Skeleton width={100} height={100} />
            <Skeleton width={100} height={100} />
            <Skeleton width={100} height={100} />
          </div>
        </div>
        .
      </div>
      <div className="img-input">
        <Field
          type="file"
          name="productImages"
          id="productImages"
          onChange={handleFileChange}
          accept="image/*"
        />
        <h5>Select product images to upload</h5>
      </div>
    </>
  );
}

useImageUpload.js

import axios from "axios";
import { useState } from "react";
import { useMutation } from "react-query";

const useImageUpload = () => {
  const [images, setImages] = useState([]);

  let { isLoading: uploading, mutateAsync: uploadImage } = useMutation(
    async (data) => {
      let formData = new FormData();
      formData.append("file", data);
      formData.append("upload_preset", "uploadPresetHere");
      formData.append("cloud_name", "nameHere");

      return await axios.post(
        "cloudinary_url",
        formData,
        {
          withCredentials: false,
        }
      );
    },
    {
      onSuccess: ({ data }) => {
        console.log("Image uploaded successfully");
        setImages([...images, data.secure_url]);
      },
      onError: (error) => {
        console.log(error.response);
      },
    }
  );

  return { images, uploadImage, uploading };
};

export default useImageUpload;

Sorry if I have made any mistake in the question.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

如果没结果 2025-01-17 10:59:19

这是 React Query 的正常行为:如果使用 useMutation 执行突变的组件卸载,onSuccess 回调仍然会被调用。

要修复它,您有多种选择,您可以:
在 ref 中跟踪组件的已安装/已卸载状态:

  const mounted = useRef(true)

  useEffect(() => {
    mounted.current = true
    () => mounted.current = false
  })

  let { ... } = useMutation(
    async (data) => {...},
    {
      onSuccess: ({ data }) => {
        if (mounted.current) {
           setImages([...images, data.secure_url]);
        }
      }
    }
  );

但不确定它在所有情况下都有效,通常 React 在卸载组件时会删除 refs。

另一个解决方案是将图像存储在 React Query 缓存中。毕竟它的数据来自服务器,所以它是服务器状态并且应该驻留在缓存中:

  let { ... } = useMutation(
    async (data) => {...},
    {
      onSuccess: ({ data }) => {
         queryClient.setQueryData('images', prev => [...prev, data])
      }
    }
  );

This is a normal behaviour of React Query : if a component performing a mutation using useMutation unmounts, the onSuccess callback still gets called.

To fix it you have several options, you can :
Keep track of the mounted / unmounted state of the component in a ref :

  const mounted = useRef(true)

  useEffect(() => {
    mounted.current = true
    () => mounted.current = false
  })

  let { ... } = useMutation(
    async (data) => {...},
    {
      onSuccess: ({ data }) => {
        if (mounted.current) {
           setImages([...images, data.secure_url]);
        }
      }
    }
  );

Not sure it works in all cases though, normally React deletes refs when unmounting a component.

Another solution is to store the images in the React Query cache. After all it's data coming from the server, so it's server state and should reside in the cache :

  let { ... } = useMutation(
    async (data) => {...},
    {
      onSuccess: ({ data }) => {
         queryClient.setQueryData('images', prev => [...prev, data])
      }
    }
  );
写下不归期 2025-01-17 10:59:18

此答案中所述,即使您的组件卸载,useMutation上的回调始终会被调用。但是,您可以在 .mutate() 上使用附加回调,因为只有在突变完成时组件仍处于挂载状态时才会调用这些回调。 此处对此进行了记录。


此外,此警告是一个红色警告,该警告将在 React 18 中删除,如 React 团队在 React 18 工作组。据称,在已卸载的组件中调用 setState 实际上并不是内存泄漏,而且完全是一件好事,而建议的使用 ref 的解决方法实际上是比原来的问题更糟糕(订阅外部商店时忘记在 useEffects 中取消订阅 - 这可能发生在库中,但对于应用程序开发人员来说不太可能发生)。

As stated in this answer, the callbacks on useMutation are always called, even if your component unmounts. However, you can use the additional callback on .mutate(), as these callbacks will only be called if the component is still mounted when the mutation finishes. This is documented here.


Further, this warning is a red hering, and the warning will be removed in React 18, as documented by the react team in the React 18 working group. It is stated that calling setState in an already unmounted component is not in fact a memory leak and is a totally fine thing to do, while the proposed workaround with a ref is actually worse than the original problem (which was forgetting to unsubscribe in useEffects when subscribing to external stores - something that can happen in libraries, but is very unlikely to happen for app developers).

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文