通过 React useEffect Hook 更新状态时防止无限循环
Prevent infinite loop when updating state via React useEffect Hook
我似乎总是 运行 解决这个问题,而且我从来没有能够很好地解决这个问题以有效地处理它。
我将使用基于下面代码块的示例,它表示使用 useState 和 useEffect 的 React 功能组件。
设置
AWS S3 存储桶中有一些 mp3 文件。这些已经过处理,因此文件名的格式为 "artist ||| title.mp3".
这些歌曲的元数据已存储在 DyanamoDB table 中,分区键为 "artist",排序键为 "title"。
有一个函数 getS3songs,它以对象数组的形式异步获取所有歌曲的列表,其中包含一个键 "key",它保存来自#2 以上。
相同的函数 运行s a forEach
在文件列表中并从每个文件中解析出 "artist" 和 "title",然后单独执行异步 API 调用以通过 API 网关从 DynamoDB table 获取每首歌曲的元数据。
使用 React useState 挂钩在状态中创建数组 "songs"。
我想做什么
我最终想做的是使用 useEffect 挂钩为 "songs" 数组填充元数据 return 为步骤 #4 中的每首歌曲编辑。
问题
以下代码块导致无限循环 运行,因为 [songs] 被设置为 useEffect 挂钩的第二个参数。
我已经尝试了几种变体,但我相信以下代表了我正在努力解决的问题的关键。
注意 这里棘手的部分不是 "s3Songs" 的初始获取。这可以作为单个对象直接放入状态。棘手的部分是有多个异步调用 API 来获取每个文件的元数据并将这些对象中的每一个都放入 "songs" 数组中。这就是我的想法。
问题
此问题的最佳或推荐模式是什么?
import React, { useEffect, useState } from "react";
import Amplify, { API, Storage } from "aws-amplify";
import awsconfig from "./aws-exports";
Amplify.configure(awsconfig);
const WhosebugExample = () => {
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 WhosebugExample;
更新 基于 Will 关于分成两个 useEffect 挂钩的评论。我已经尝试了下面的方法,我在想我是否可以将 Promise.all
加入混音中,以便 return 第二个钩子中的 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]);
看来您需要通过添加另一个 useEffect
来分离两个提取的关注点。然后,您可以使用 Promise.all
等待第二次 api 调用的响应完成,然后再更新您的歌曲。
const WhosebugExample = () => {
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>
);
};
我似乎总是 运行 解决这个问题,而且我从来没有能够很好地解决这个问题以有效地处理它。
我将使用基于下面代码块的示例,它表示使用 useState 和 useEffect 的 React 功能组件。
设置
AWS S3 存储桶中有一些 mp3 文件。这些已经过处理,因此文件名的格式为 "artist ||| title.mp3".
这些歌曲的元数据已存储在 DyanamoDB table 中,分区键为 "artist",排序键为 "title"。
有一个函数 getS3songs,它以对象数组的形式异步获取所有歌曲的列表,其中包含一个键 "key",它保存来自#2 以上。
相同的函数 运行s a
forEach
在文件列表中并从每个文件中解析出 "artist" 和 "title",然后单独执行异步 API 调用以通过 API 网关从 DynamoDB table 获取每首歌曲的元数据。使用 React useState 挂钩在状态中创建数组 "songs"。
我想做什么 我最终想做的是使用 useEffect 挂钩为 "songs" 数组填充元数据 return 为步骤 #4 中的每首歌曲编辑。
问题 以下代码块导致无限循环 运行,因为 [songs] 被设置为 useEffect 挂钩的第二个参数。
我已经尝试了几种变体,但我相信以下代表了我正在努力解决的问题的关键。
注意 这里棘手的部分不是 "s3Songs" 的初始获取。这可以作为单个对象直接放入状态。棘手的部分是有多个异步调用 API 来获取每个文件的元数据并将这些对象中的每一个都放入 "songs" 数组中。这就是我的想法。
问题 此问题的最佳或推荐模式是什么?
import React, { useEffect, useState } from "react";
import Amplify, { API, Storage } from "aws-amplify";
import awsconfig from "./aws-exports";
Amplify.configure(awsconfig);
const WhosebugExample = () => {
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 WhosebugExample;
更新 基于 Will 关于分成两个 useEffect 挂钩的评论。我已经尝试了下面的方法,我在想我是否可以将 Promise.all
加入混音中,以便 return 第二个钩子中的 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]);
看来您需要通过添加另一个 useEffect
来分离两个提取的关注点。然后,您可以使用 Promise.all
等待第二次 api 调用的响应完成,然后再更新您的歌曲。
const WhosebugExample = () => {
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>
);
};