为什么我的React应用在使用`useEffect`时会出现重新渲染循环?

huangapple go评论115阅读模式
英文:

why my react app has re-rendering loop while using useEffect

问题

我目前正在编写一个代码,用于更新以前帖子的内容。

其中有一部分,我将从服务器接收到的图像URL转换为文件并存储在状态中。

然而,通过这种编码方法,我遇到了过多的重新渲染问题。

我不确定该怎么办了。最初可能会有多个URL,所以我使用了useQueries。这可能是问题吗?

总结一下:

在初始渲染期间,

  • 从服务器获取先前帖子的信息。
  • 将接收到的信息存储在状态中。
  • 将图像URL数组转换为文件数组。
  • 将转换后的文件存储在状态中。

如果您有任何信息可以帮助解决这个问题,比如过多重新渲染的原因或可能的解决方案,请告诉我。

在控制台中,当我记录日志时,我注意到发生了过多的重新渲染,并且数据未存储在setUploadedImages中。

英文:

I'm currently working on a code to update the content of a previous post.

Among them, there is a part where I convert the image URLs received from the server into files and store them in state.

However, with this coding approach, I'm experiencing excessive re-renders.

I'm not sure what to do anymore. Initially, there can be multiple URLs, so I used useQueries.
Could that be the problem?

In summary:

During the initial rendering,

  • fetch the information of the previous post from the server.
  • Store the received information in state.
  • Convert the array of image URLs into an array of files.
  • Store the converted files in state.

If you have any information to help solve this problem, such as the cause of the excessive re-renders or possible solutions, please let me know.

const ShellUpdate: React.FC = () => {
  const { id } = useParams();
  const { data } = useGetShells(parseInt(id as string));
  const urls = data?.data.pictures.map((item: { url: string }) => item.url);
  const ImagesDate = useImageUpload(urls);
  const [uploadedImages, setUploadedImages] = useState<File[]>([]);

  useEffect(() => {
    if (ImagesDate[0].status === 'success') {
      const files = ImagesDate.map((queryResult) => queryResult?.data).filter(
        Boolean
      ) as FileWithPath[];
      console.log(files);
      setUploadedImages(files);
    }
  }, []);
}

export const useImageUpload = (
  updateInitalImages: string[]
): QueryObserverResult<FileWithPath>[] => {

  const queryResults = useQueries(
    updateInitalImages.map((imageUrl) => ({
      queryKey: [queryKeys.imageData, imageUrl],
      queryFn: () => getImageFile(imageUrl),
    }))
  );


  return queryResults;
};


When I console logged, I noticed excessive re-renders occurring, and the data was not getting stored in setUploadedImages.

答案1

得分: 1

以下是翻译好的部分:

问题出在 useEffect 的依赖数组上,首先你没有提供任何变量给它,这意味着 useEffect 回调函数将在组件元素渲染后运行一次,这意味着你会得到一个旧版本的 ImagesDate(从技术角度来说,它属于陈旧的闭包),所以为了修复这个问题,你可能会想,好吧,让我们把它传递到依赖数组中:

    useEffect(() => {
  
      if (ImagesDate?.[0]?.status === 'success') {

        const files = ImagesDate
          ?.filter(queryResult => Boolean(queryResult.data))
          ?.map(queryResult => queryResult.data)
          ?? [];
  
        //console.log(files);
        setUploadedImages(files as File[]);
      }
  
    }, [ImagesDate]);

嗯,这通常没问题,但是在依赖数组中包含对象(也包括数组或函数)非常危险,原因是 Referential Equality,这意味着 React 将根据 Object.is 进行依赖数组的比较,而对于对象,它始终返回 false,因为在 JavaScript 中:

{} === {} // false
Object.is({}, {}) // false

这将导致 useEffect 回调在每次组件重新渲染时都会运行,你可能会问,但我不会触发重新渲染吗?!好吧,这行代码的使用:

setUploadedImages(files as File[]);

当然会触发重新渲染,这意味着你会得到无限循环的重新渲染...

有哪些可能的解决方法?
我建议的可能最简单的解决方法是将 uploadedImages 不作为受控状态,而是将其作为 派生状态(derived state),因为你可以基于另一个状态来获取其值,没有必要为它设置一个 setter 函数并设置一个副作用来进行设置,所以这应该足够了:

  //  const [uploadedImages, setUploadedImages] = useState<File[]>([]);

  const uploadedImages = ImagesDate
    ?.filter(queryResult => Boolean(queryResult.data))
    ?.map(queryResult => queryResult.data)
    ?? [];

  /*
    useEffect(() => {
  
      if (ImagesDate?.[0]?.status === 'success') {

        const files = ImagesDate
          ?.filter(queryResult => Boolean(queryResult.data))
          ?.map(queryResult => queryResult.data)
          ?? [];
  
        //console.log(files);
        setUploadedImages(files as File[]);
      }
  
    }, [ImagesDate]);
    */

还有一点与 useQueries 相关,在 文档 中提到,你应该向 useQueries 传递一个 queries 属性:

export const useImageUpload = (updateInitalImages: string[]) => {

  const queryResults = useQueries(
    {
      queries:
        updateInitalImages.map(imageUrl => ({
          queryKey: ['convert to image file', imageUrl],
          queryFn: () => {
            console.log('re calling api with', imageUrl);
            return getImageFile(imageUrl)
          },
        }))
    }
  );

  return queryResults;
};
英文:

The issue is with useEffect dependency array, first you didn't provide it with any variable, which mean the useEffect callback function will run one time after the component elements are rendered, which mean you will have an old version of ImagesDate (which belong to a stale closure to be more technical), so to fix this you'd think, okay let's pass it in the dependency array:

    useEffect(() =&gt; {
  
      if (ImagesDate?.[0]?.status === &#39;success&#39;) {

        const files = ImagesDate
          ?.filter((queryResult) =&gt; Boolean(queryResult.data))
          ?.map((queryResult) =&gt; queryResult.data)
          ?? [];
  
        //console.log(files);
        setUploadedImages(files as File[]);
      }
  
    }, [ImagesDate]);

well that's normally okay, but having objects(arrays or functions too) in the dependency array is very dangerous for the reason of Referential Equality, which mean react will do comparison on the dependency array based on Object.is which for objects will always return false, because in javascript:

{} === {} // false
Object.is({}, {}) // false

so that will cause the useEffect callback to run every time the component re-render, you might ask, but I will not trigger re-render?! well, the use of this line

        setUploadedImages(files as File[]);

will of course trigger the re-render again, which mean you will get an infinite loop of re-renders...

what possible solutions to this?
the simplest solution possible that I suggest is not to have uploadedImages as controlled state, instead have it as derived state because you can have it's value based on another state, there's no need to have a setter function for that and have to go all the way to setup a side effect to set it, so this should be enough for it:

  //  const [uploadedImages, setUploadedImages] = useState&lt;File[]&gt;([]);

  const uploadedImages = ImagesDate
    ?.filter((queryResult) =&gt; Boolean(queryResult.data))
    ?.map((queryResult) =&gt; queryResult.data)
    ?? [];

  /*
    useEffect(() =&gt; {
  
      if (ImagesDate?.[0]?.status === &#39;success&#39;) {

        const files = ImagesDate
          ?.filter((queryResult) =&gt; Boolean(queryResult.data))
          ?.map((queryResult) =&gt; queryResult.data)
          ?? [];
  
        //console.log(files);
        setUploadedImages(files as File[]);
      }
  
    }, [ImagesDate]);
    */


one more point related to useQueries, following the documentation, you should pass a queries property to useQueries

export const useImageUpload = (updateInitalImages: string[]) =&gt; {

  const queryResults = useQueries(
    {
      queries:
        updateInitalImages.map((imageUrl) =&gt; ({
          queryKey: [&#39;convert to image file&#39;, imageUrl],
          queryFn: () =&gt; {
            console.log(&#39;re calling api with&#39;, imageUrl);
            return getImageFile(imageUrl)
          },
        }))
    }
  );


  return queryResults;
};

huangapple
  • 本文由 发表于 2023年7月17日 16:31:44
  • 转载请务必保留本文链接:https://go.coder-hub.com/76702699.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定