[英]Prevent infinite loop when updating state via React useEffect Hook
I always seem to run into this problem, and I've never been able to wrap my head around it well enough to deal with it effectively. 我似乎总是遇到这个问题,而且我从来没有能够很好地解决它,以便有效地处理它。
I'll use an example based on the code block below, which represents a React functional component that uses useState and useEffect. 我将使用基于下面代码块的示例,它代表一个使用useState和useEffect的React功能组件。
Setup 设定
There are some mp3 files located in an AWS S3 bucket. AWS S3存储桶中有一些mp3文件。 These have been processed so that the file names are in the format of "artist ||| title.mp3".
这些已经过处理,因此文件名的格式为“artist ||| title.mp3”。
Metadata for those songs has been stored in a DyanamoDB table with a partition key of "artist" and a sort key of "title". 这些歌曲的元数据已存储在DyanamoDB表中,分区键为“artist”,排序键为“title”。
There is a function, getS3songs, that asynchronously gets a list of all songs in the form of an array of objects, with a key, "key", that holds the file name from #2 above. 有一个函数getS3songs,它以一个对象数组的形式异步获取所有歌曲的列表,其中包含一个键“key”,它保存上面#2的文件名。
That same function runs a forEach
on the file list and parses out "artist" and "title" from each file, then does a separate asynchronous API call to get the metadata for each song from a DynamoDB table via an API Gateway. 该函数在文件列表上运行
forEach
并从每个文件中解析出“artist”和“title”,然后执行单独的异步API调用,以通过API网关从DynamoDB表中获取每首歌曲的元数据。
An array, "songs", is created in state, using a React useState hook. 使用React useState钩子在状态中创建数组“songs”。
What I'm trying to do What I'm ultimately trying to do is use a useEffect hook to populate the "songs" array with the metadata returned for each song from step #4. 我正在尝试做什么我最终要做的是使用useEffect钩子来填充“songs”数组,其中包含从步骤#4为每首歌曲返回的元数据。
The problem The following code block results in an infinite loop being run, as [songs] is set as second parameter of the useEffect hook. 问题以下代码块导致运行无限循环,因为[songs]被设置为useEffect挂钩的第二个参数。
I've tried several variations, but I believe the below represents the crux of the problem I'm trying to work through. 我尝试了几种变体,但我相信下面代表了我正在尝试解决的问题的症结所在。
Note The tricky part here isn't the initial get of the "s3Songs". 注意这里棘手的部分不是“s3Songs”的初始获取。 This could be put straight into state as a single object.
这可以作为单个对象直接进入状态。 The tricky part is that there are multiple asynchronous calls to an API to get the metadata for each file and getting each one of these objects into the "songs" array.
棘手的部分是,有多个异步调用API来获取每个文件的元数据,并将这些对象中的每一个都放入“歌曲”数组中。 And this is what's doing my head in.
而这就是我的目标。
Question What is the best or recommended pattern for this problem? 问题此问题的最佳或推荐模式是什么?
import React, { useEffect, useState } from "react";
import Amplify, { API, Storage } from "aws-amplify";
import awsconfig from "./aws-exports";
Amplify.configure(awsconfig);
const StackOverflowExample = () => {
const [songs, setSongs] = useState([]);
useEffect(() => {
const getS3Songs = async () => {
const s3Songs = await Storage.list("", { level: "public" });
s3Songs.forEach(async song => {
const artist = song.key.split(" ||| ")[0];
const title = song.key.split(" ||| ")[1].slice(0, -4);
const metadata = await API.get("SongList", `/songs/${artist}/${title}`);
// setSongs([...songs, metadata]); <= causes loop
});
};
getS3Songs();
}, [songs]); // <= "songs" auto added by linter in create-react-app in vsCode. Removing "songs" and disabling linter on that line doesn't help.
const renderSongs = () => {
return songs.map(song => {
return <li>{song.title}</li>;
});
};
return (
<div>
<ul>{renderSongs()}</ul>
</div>
);
};
export default StackOverflowExample;
Update Based on Will's comment regarding separating into two useEffect hooks. 更新根据Will关于分成两个useEffect挂钩的注释。 I have tried the below, and I'm thinking if I can work a
Promise.all
into the mix so as to return the songMetadata array in the second hook, after it's populated when all metadata
promises have resolved, I'd be getting close. 我已经尝试了下面的内容,我正在考虑是否可以将
Promise.all
用于混合以便在第二个钩子中返回songMetadata数组,在所有metadata
承诺解决后填充它时,我会越来越近。
useEffect(() => {
console.log("Effect!");
const getS3Files = async () => {
try {
const response = await Storage.list("", { level: "public" });
setS3FileList(response);
} catch (e) {
console.log(e);
}
};
getS3Files();
}, []);
useEffect(() => {
console.log("Effect!");
const songMetadata = [];
s3FileList.forEach(async song => {
const artist = song.key.split(" ||| ")[0];
const title = song.key.split(" ||| ")[1].slice(0, -4);
const metadata = await API.get(
"SongList",
`/songs/object/${artist}/${title}`
);
console.log(metadata);
songMetadata.push(metadata);
});
setSongs(songMetadata);
}, [s3FileList]);
It looks like you need to separate the concerns of your two fetches by adding another useEffect
. 看起来您需要通过添加另一个
useEffect
来分离两个提取的问题。 Then you can use Promise.all
to wait for the responses from your second api call to complete before updating your songs. 然后,您可以使用
Promise.all
在更新歌曲之前等待第二次api通话的响应完成。
const StackOverflowExample = () => {
const [songs, setSongs] = useState([]);
const [s3Songs, setS3Songs] = useState([]);
const getSong = async song => {
const artist = song.key.split(" ||| ")[0];
const title = song.key.split(" ||| ")[1].slice(0, -4);
const metadata = await API.get("SongList", `/songs/${artist}/${title}`);
return metadata
}
useEffect(() => {
const getS3Songs = async () => {
const s3s = await Storage.list("", { level: "public" })n
setS3Songs(s3s);
};
getS3Songs();
}, []);
useEffect(()=>{
const pending = s3Songs.map(song=>getSong(song))
Promise.all(pending).then(songs=>setSongs(songs));
}, [s3Songs])
const renderSongs = () => {
return songs.map(song => {
return <li>{song.title}</li>;
});
};
return (
<div>
<ul>{renderSongs()}</ul>
</div>
);
};
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.