Мне всегда кажется, что я сталкиваюсь с этой проблемой, и мне никогда не удавалось обхватить ее достаточно хорошо, чтобы эффективно с ней справиться.
Я буду использовать пример, основанный на блоке кода ниже, который представляет функциональный компонент React, использующий useState и useEffect.
Настройка
В корзине AWS S3 есть несколько mp3-файлов. Они были обработаны так, что имена файлов имеют формат «artist ||| title.mp3».
Метаданные для этих песен были сохранены в таблице DyanamoDB с ключом разделения «artist» и ключом сортировки «title».
Есть функция getS3songs, которая асинхронно получает список всех песен в виде массива объектов с ключом «ключ», который содержит имя файла из # 2 выше.
Эта же функция запускает forEach
в списке файлов и анализирует «исполнитель» и «заголовок» из каждого файла, затем выполняет отдельный асинхронный вызов API для получения метаданных для каждой песни из таблицы DynamoDB. через шлюз API.
Массив "song" создается в состоянии с помощью перехватчика React useState.
Что я пытаюсь сделать
В конечном итоге я пытаюсь использовать хук useEffect, чтобы заполнить массив «песен» метаданными, возвращаемыми для каждой песни из шага № 4.
Проблема
Следующий кодовый блок приводит к выполнению бесконечного цикла, поскольку [песни] устанавливается как второй параметр ловушки 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 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;
Обновление На основании комментария Уилла относительно разделения на две ловушки useEffect. Я попробовал нижеприведенное, и я думаю, что если я смогу добавить 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]);