Я изучаю 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]);
});
}
});
«Я попробовал связать .then» — это тоже должно сработать. Можете ли вы показать нам, что именно вы пробовали?
@Bergi Можете ли вы рассказать об этом подробнее? Итак, первый useEffect выполняет вызов get_customers без каких-либо зависимостей, а второй выполняет вызовы get_site_by_type внутри цикла с клиентами в качестве зависимости? Не могли бы вы объяснить, как/почему это работает?
«вызовы асинхронны, поэтому это приводит к состоянию гонки и не работает» - у вас есть (независимо от того, связываете ли then
или используете отдельные эффекты или что-то еще) проблема с вызовами client.get_site_by_type
в цикле: вызов setLogos(fetched_logos)
не ждет никаких из них. Вам нужно будет использовать там Promise.all
(или обновлять состояние после каждого отдельного ответа, но это сложнее)
«Можете ли вы объяснить, как/почему это работает?» - он работает более или менее так же, как ваш текущий код, только без «использования операторов if для управления тем, что выполняется во время последующих вызовов useEffect», что действительно довольно хакерски
@Bergi Я отредактировал сообщение, добавив фрагмент, показывающий мою попытку связать .then. С этим кодом логотипы становятся совсем странными: первая запись не определена и куча повторяющихся записей.
Ах, да, проблема в отдельных обновлениях setLogos([...logos, new_logo]);
(поскольку logos
всегда пустой массив), сам then
работает нормально. Опять же, я рекомендую использовать Promise.all
, но вы также можете использовать setLogos(oldLogos => oldLogos.toSpliced(i, 1, new_logo));
@Bergi Извините, но я до сих пор не совсем понимаю, как все это собрать воедино… не могли бы вы поделиться фрагментом кода?
Любимая недовольство: мне действительно не нравится просить о «лучшей практике» в стольких словах. Просто спросите: «Как я могу асинхронно получать данные API из разных конечных точек с помощью React?» и определите столько требований, сколько вам нужно, чтобы убедиться, что он «лучший» (удовлетворяет всем вашим требованиям).
Две проблемы: одна из них заключается в том, что переменные состояния не обновляются сразу после вызова функции 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. Теперь он застрял в бесконечном цикле. Я правда не понимаю, что происходит... мне так сложно представить это
Бесконечный цикл возник из-за отсутствия массива зависимостей. Добавление пустого массива зависимостей исправило ситуацию.
Да, часть карты требует либо неявного, либо явного возврата. Мой пример имеет неявный возврат.
Вот рабочий компонент:
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 пуст, если логотип не найден.
Этот ответ требует более четкого объяснения того, что вы изменили. Код без пояснений и дополнительных комментариев бесполезен.
Используйте два эффекта, при этом состояние первого изменяется в зависимости от второго.