Предотвратить бесконечный цикл при обновлении состояния через React useEffect Hook - PullRequest
0 голосов
/ 01 июля 2019

Мне всегда кажется, что я сталкиваюсь с этой проблемой, и мне никогда не удавалось обхватить ее достаточно хорошо, чтобы эффективно с ней справиться.

Я буду использовать пример, основанный на блоке кода ниже, который представляет функциональный компонент React, использующий useState и useEffect.

Настройка

  1. В корзине AWS S3 есть несколько mp3-файлов. Они были обработаны так, что имена файлов имеют формат «artist ||| title.mp3».

  2. Метаданные для этих песен были сохранены в таблице DyanamoDB с ключом разделения «artist» и ключом сортировки «title».

  3. Есть функция getS3songs, которая асинхронно получает список всех песен в виде массива объектов с ключом «ключ», который содержит имя файла из # 2 выше.

  4. Эта же функция запускает forEach в списке файлов и анализирует «исполнитель» и «заголовок» из каждого файла, затем выполняет отдельный асинхронный вызов API для получения метаданных для каждой песни из таблицы DynamoDB. через шлюз API.

  5. Массив "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]);

1 Ответ

1 голос
/ 01 июля 2019

Похоже, вам нужно разделить заботы о двух ваших выборках, добавив еще один useEffect.Затем вы можете использовать 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 s3Songs = await Storage.list("", { level: "public" });
    };
    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>
  );
};
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...