Лучшая практика асинхронного получения данных API из разных конечных точек с помощью React?

Я изучаю React для проекта на работе, и в настоящее время у меня действительно возникают проблемы с освоением асинхронной модели.

Вот компонент, над которым я работаю:

const Customers = () => {
    const [logos, setLogos] = useState([]);
    const [customers, setCustomers] = useState([]);
    const [titlePrompt, setTitlePrompt] = useState([]);
    const navigate = useNavigate();

    useEffect(() => {
        // create a customer client object 
        const client = new Client();

        // query for the customer list and save name and token
        if (customers.length === 0) {
            client.get_customers().then((res) => {
                const customer_info = res.map(customer => {
                    return {
                        'name': customer.customer_name,
                        'token': customer.mqtt_customer_token
                    }
                });
                setCustomers(customer_info);
            });
        }

        if (customers.length > 0) {
            const fetched_logos = [];
            for (let i = 0; i < customers.length; i++) {
                client.get_site_by_type(customers[i].token, 1).then((res) => {
                    const new_logo = {
                        [customers[i].name]: res.logo 
                    };
                    fetched_logos.push(new_logo);
                });
            }
            setLogos(fetched_logos);
        }

        setTitlePrompt("Select A Customer Shown Below");
    }, [customers, logos]);

    /**
     * button handler to transition to a dataframe view of the chosen customer via token
     * @param {defining} token 
     */
    const onButtonClick = (token) => {
        navigate('/dataframe', { state: { token } });
    };

    return (
        <div className = {'mainContainer'}>
            <div className = {'contentContainer'}>
                <div className = {'titleContainer'}>
                    <div>{titlePrompt}</div>
                </div>
                <br/>
                <ImageList cols = {4}>
                    {customers.map(customer => (
                        <ImageListItem key = {customer.name}>
                            <img alt = {customer.name} src = {`data:image/png;base64,${logos[customer.name]}`}></img>
                        </ImageListItem>
                    ))}
                </ImageList>
            </div>
        </div>
    );
};

export default Customers;

Я пытаюсь получить информацию о клиенте, такую ​​как имя и логотип, и отобразить ее в ImageList.

Проблема в том, что сначала мне нужно получить имя и токен (используемый для последующих вызовов API) для каждого клиента из одной базы данных, затем мне нужно получить логотипы один за другим, поскольку все они находятся в конкретных базах данных клиентов. Я хочу визуализировать компонент только с помощью замещающего текста компонента img, а затем заполнять изображения по мере их поступления. Я просто не могу понять, как правильно синхронизировать это внутри useEffect. Я также не знаю, правильно ли использовать операторы if для управления тем, что выполняется во время последующих вызовов useEffect, или это просто хакерство.

Я попробовал связать .then, но вызовы setState являются асинхронными, поэтому это приводит к состояниям гонки и не работает.

Спасибо за чтение.

Обновлено:

Вот фрагмент кода, показывающий мою попытку с цепочкой .then:

client.get_customers().then((res) => {
            const customer_info = res.map(customer => {
                return {
                    'name': customer.customer_name,
                    'token': customer.mqtt_customer_token
                }
            });
            setCustomers(customer_info);
        }).then(() => {
            for (let i = 0; i < customers.length; i++) {
                client.get_site_by_type(customers[i].token, 1).then((res) => {
                    const new_logo = {
                        [customers[i].name]: res[0].logo
                    }
                    setLogos([...logos, new_logo]);
                });
            }
        });

Используйте два эффекта, при этом состояние первого изменяется в зависимости от второго.

Bergi 28.06.2024 21:34

«Я попробовал связать .then» — это тоже должно сработать. Можете ли вы показать нам, что именно вы пробовали?

Bergi 28.06.2024 21:35

@Bergi Можете ли вы рассказать об этом подробнее? Итак, первый useEffect выполняет вызов get_customers без каких-либо зависимостей, а второй выполняет вызовы get_site_by_type внутри цикла с клиентами в качестве зависимости? Не могли бы вы объяснить, как/почему это работает?

Logan 28.06.2024 21:37

«вызовы асинхронны, поэтому это приводит к состоянию гонки и не работает» - у вас есть (независимо от того, связываете ли then или используете отдельные эффекты или что-то еще) проблема с вызовами client.get_site_by_type в цикле: вызов setLogos(fetched_logos) не ждет никаких из них. Вам нужно будет использовать там Promise.all (или обновлять состояние после каждого отдельного ответа, но это сложнее)

Bergi 28.06.2024 21:37

«Можете ли вы объяснить, как/почему это работает?» - он работает более или менее так же, как ваш текущий код, только без «использования операторов if для управления тем, что выполняется во время последующих вызовов useEffect», что действительно довольно хакерски

Bergi 28.06.2024 21:40

@Bergi Я отредактировал сообщение, добавив фрагмент, показывающий мою попытку связать .then. С этим кодом логотипы становятся совсем странными: первая запись не определена и куча повторяющихся записей.

Logan 28.06.2024 22:05

Ах, да, проблема в отдельных обновлениях setLogos([...logos, new_logo]); (поскольку logos всегда пустой массив), сам then работает нормально. Опять же, я рекомендую использовать Promise.all, но вы также можете использовать setLogos(oldLogos => oldLogos.toSpliced(i, 1, new_logo));

Bergi 28.06.2024 22:12

@Bergi Извините, но я до сих пор не совсем понимаю, как все это собрать воедино… не могли бы вы поделиться фрагментом кода?

Logan 28.06.2024 22:57

Любимая недовольство: мне действительно не нравится просить о «лучшей практике» в стольких словах. Просто спросите: «Как я могу асинхронно получать данные API из разных конечных точек с помощью React?» и определите столько требований, сколько вам нужно, чтобы убедиться, что он «лучший» (удовлетворяет всем вашим требованиям).

Wyck 02.07.2024 22:52
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
1
9
67
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Две проблемы: одна из них заключается в том, что переменные состояния не обновляются сразу после вызова функции set. Лучше использовать переменную, содержащую полученные данные (здесь я возвращаю customer_info, чтобы ее можно было использовать в then, вместо того, чтобы вызывать setCustomers и затем ожидать обновления константных клиентов, чего не произошло).

Вторая проблема заключается в том, что для правильной синхронизации вам нужно использовать Promise.all или аналогичный.

Непроверенный иллюстративный код:

client.get_customers().then((res) => {
  const customer_info = res.map(customer => {
    return {
      'name': customer.customer_name,
      'token': customer.mqtt_customer_token
    }
  });
  setCustomers(customer_info);
  return customer_info;
}).then((customer_info) => {
  const promises = customer_info.map(customer => 
    client.get_site_by_type(customer.token, 1).then((res) => {
      const new_logo = {
        [customer.name]: res[0].logo
      }
      return new_logo;
    })
  );
  Promise.all(promises).then(logos => setLogos(logos));
});

Я попробовал это, мне показалось, что все это имело смысл, за исключением того, что мне пришлось добавить оператор return перед вызовом client.get_site_by_type, потому что мой линтер кричал на меня за то, что я не возвращал что-то в вызове .map. Теперь он застрял в бесконечном цикле. Я правда не понимаю, что происходит... мне так сложно представить это

Logan 28.06.2024 23:30

Бесконечный цикл возник из-за отсутствия массива зависимостей. Добавление пустого массива зависимостей исправило ситуацию.

Logan 29.06.2024 00:05

Да, часть карты требует либо неявного, либо явного возврата. Мой пример имеет неявный возврат.

James 29.06.2024 11:41

Вот рабочий компонент:

const Customers = () => {
    const [logos, setLogos] = useState([]);
    const [customers, setCustomers] = useState([]);
    const navigate = useNavigate();

    useEffect(() => {
        // create a customer client object 
        let client = new Client();

        // query for the customer list and save name and token
        client.get_customers().then((res) => {
            const fetched_customers = res.map(customer => {
                return {
                    'name': customer.customer_name,
                    'token': customer.mqtt_customer_token
                }
            });
            setCustomers(fetched_customers);
            return fetched_customers;
        }).then((fetched_customers) => {
            const promises = fetched_customers.map(customer => {
                return client.get_sites_by_type(customer.token, 1).then((res) => {
                    const new_logo = {
                        'name': customer.name,
                        'logo': res[0].logo
                    }
                    return new_logo;
                })
            });
            Promise.all(promises).then((logos) => {
                setLogos(logos);
                console.info(logos);
            })
        })

    }, []);

    /**
     * button handler to transition to a dataframe view of the chosen customer via token
     * @param {defining} token 
     */
    const onButtonClick = (token) => {
        navigate('/dataframe', { state: { token } });
    };

    return (
        <div className = {'mainContainer'}>
            <div className = {'contentContainer'}>
                <div className = {'titleContainer'}>
                    <div>Select a customer</div>
                </div>
                <br/>
                <ImageList cols = {4}>
                    {customers.map((customer) => {
                        const logo = logos.filter(logo => logo.name === customer.name)[0]?.logo ?? '';
                        return (
                            <ImageListItem key = {customer.name}>
                                <img alt = {customer.name} src = {`data:image/png;base64,${logo}`} />
                            </ImageListItem>
                        );
                    })}
                </ImageList>
            </div>
        </div>
    );
};

export default Customers;

Сначала я нахожу клиентов и устанавливаю их. Затем я использую .then для получения массива всех возвращенных обещаний, получая каждый отдельный логотип с помощью вызова .map. Затем я использую Promise.all, чтобы установить все логотипы после того, как все обещания будут выполнены. Я также изменил способ хранения логотипов, дав им имя и ключ логотипа, чтобы на них можно было правильно ссылаться при вызове .map внутри ImageList. Вызов фильтра внутри ImageList просто гарантирует, что img src пуст, если логотип не найден.

Этот ответ требует более четкого объяснения того, что вы изменили. Код без пояснений и дополнительных комментариев бесполезен.

Andy 29.06.2024 12:57

Другие вопросы по теме

Похожие вопросы

Где хранятся анонимные функции обратного вызова для setTimeout?
Добавить числа в объект в массиве JavasScript из индекса ниже значения и возвращаемого значения
Печать и предварительный просмотр веб-страницы не работают должным образом в браузере Microsoft Edge
Возврат значений на страницу Razor — запутанный Javascript или .cs?
Сборка npm run не удалась из-за несовместимости машинописного текста или lodash
TypeScript «Нет перегрузки, соответствующей этому вызову»
Способ определения точных свойств стиля, которые в данный момент применяются веб-анимацией (без смешивания со всеми применяемыми в данный момент стилями)
Как загрузить изображение на холст в Gradio с помощью специального HTML и JavaScript?
Dash.js извлекает файлы .m4s при настройке video.currentTime и приводит к ошибкам. Ошибки при настройке video.currentTime в Dash.js
Загрузка файлового потока из браузера, получение запроса