Доброго времени суток, Хабр!
Все reactjs разработчики, кто имеет дело с интерактивностью между бэком и фронтом рано или поздно встречаются, или встречались, или встретятся со следующей ошибкой:
Если дословно, то получится так:
На самом деле все достаточно просто, нужно всего лишь обратить внимание на следующие словосочетания:
В основе хуки. GO в под кат!
Итак:
Воспроизведем ошибку:
Допустим, мы разрабатываем сайт с подгрузкой описания фильмов (не будем углубляться в API moviedb, а возьмем за основу конкретный фильм). У нас есть две странички:
Ссылки на обе странички доступны в навигационной панели, например в header. На главной страничке («Домой») происходит общение с бэком для подгрузки информации о фильме.
/src/Pages/HomePage.js
/src/Pages/AboutPage.js
Для отображения информации посредством запроса необходимо записать информацию в состояние для последующего рендера:
/src/Pages/HomePage.js
Воспроизведем ошибку следующим путем — переключимся с одного маршрута на другой, то есть находясь на страничке “О нас”, мы перейдем на страничку “Домой” и незамедлительно вернемся снова на страничку “О нас” и вуаля “Can’t perform …..”.
Дело в том, что на запросы сервер не отвечает мгновенно, используется асинхронность, чтобы все таки воспроизвести запрос параллельно необходимым задачам. Но в случае быстрого возврата на страничку “О нас”, компонент “Домой” размонтируется, а значит состояние для данного компонента сброситься, но асинхронность запроса все же запустит setMovie, которого больше нет и выкинет ошибку. Оптимальным решением является запретить использовать обновление состояния при размонтировании компонента:
/src/Pages/HomePage.js
Итого:
Весь исходный код можно посмотреть здесь: https://gitlab.com/ImaGadzhiev/react-cant-perform
Все reactjs разработчики, кто имеет дело с интерактивностью между бэком и фронтом рано или поздно встречаются, или встречались, или встретятся со следующей ошибкой:
Если дословно, то получится так:
Предупреждение. Невозможно выполнить обновление состояния React для неустановленного компонента. Это не операция, но она указывает на утечку памяти в вашем приложении. Чтобы исправить, отмените все подписки и асинхронные задачи в функции очистки useEffect.
На самом деле все достаточно просто, нужно всего лишь обратить внимание на следующие словосочетания:
- Невозможно выполнить обновление состояния
- неустановленного компонента;
- отмените все подписки и асинхронные задачи
- очистки useEffect
В основе хуки. GO в под кат!
Итак:
- Все reactjs программисты знают что такое состояние (state) и что такое обновление(setState) тоже. Я не буду этом.
- Неустановленный компонент. Учитывая контекст ошибки первое что приходит на ум:
1) компонент которого нет;
2) компонент который мы не импортировали;
3) компонент который размонтировался;
Наш случай — 3 пункт. Без комментарий.
- Подписки и асинхронные задачи. То есть функции которые что-то выполняют, например: изменяют состояние. Что касаемо асинхронных задач, то тут сразу на ум бросается async/await — это наш способ общения с бэком в побочном эффекте.
- Очистка useEffect. Я думаю все знают что такое return () => {} в useEffect. Таким образом, мы можем произвести какие-либо действия в возвращаемой функции эффекта при размонтировании компонента, например: запретить изменять состояние.
Воспроизведем ошибку:
Допустим, мы разрабатываем сайт с подгрузкой описания фильмов (не будем углубляться в API moviedb, а возьмем за основу конкретный фильм). У нас есть две странички:
Домой
О нас
Ссылки на обе странички доступны в навигационной панели, например в header. На главной страничке («Домой») происходит общение с бэком для подгрузки информации о фильме.
/src/Pages/HomePage.js
import React, { useState, useEffect } from 'react';
import { MOVIE_DB_GET } from '../config';
const HomePage = () => {
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(MOVIE_DB_GET);
const result = await response.json();
console.log(result, 'result')
} catch (e) {
console.error(e.message)
}
};
// Произведем get-запрос на информацию о конкретном фильме
fetchData();
}, []);
return (
<main>
<h2>Главная страница</h2>
</main>
)
};
export default HomePage;
/src/Pages/AboutPage.js
import React from 'react';
const AboutPage = () => {
return (
<main>
<h2>О нас</h2>
<p>
Некий контент
</p>
</main>
)
};
export default AboutPage;
Для отображения информации посредством запроса необходимо записать информацию в состояние для последующего рендера:
/src/Pages/HomePage.js
import React, { useState, useEffect } from 'react';
import { MOVIE_DB_GET } from '../config';
const HomePage = () => {
// movie - react-состояние;
// setMovie - функция обновления react-состояния
const [ movie, setMovie ] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(MOVIE_DB_GET);
const result = await response.json();
console.log(result, 'result');
setMovie(result);
} catch (e) {
console.error(e.message)
}
};
// Произведем запрос на информацию о конкретном фильме
fetchData();
}, []);
// descriptionMovie - некая функция, возвращающая view, то есть рендерит информацию о фильме посредством состояния movie
return (
<main>
<h2>Главная страница</h2>
<p>Описание фильма</p>
{
movie ? descriptionMovie() : false
}
</main>
)
};
export default HomePage;
Воспроизведем ошибку следующим путем — переключимся с одного маршрута на другой, то есть находясь на страничке “О нас”, мы перейдем на страничку “Домой” и незамедлительно вернемся снова на страничку “О нас” и вуаля “Can’t perform …..”.
Дело в том, что на запросы сервер не отвечает мгновенно, используется асинхронность, чтобы все таки воспроизвести запрос параллельно необходимым задачам. Но в случае быстрого возврата на страничку “О нас”, компонент “Домой” размонтируется, а значит состояние для данного компонента сброситься, но асинхронность запроса все же запустит setMovie, которого больше нет и выкинет ошибку. Оптимальным решением является запретить использовать обновление состояния при размонтировании компонента:
/src/Pages/HomePage.js
import React, { useState, useEffect } from 'react';
import { MOVIE_DB_GET } from '../config';
const HomePage = () => {
// movie - react-состояние;
// setMovie - функция обновления react-состояния
const [ movie, setMovie ] = useState(null);
useEffect(() => {
let cleanupFunction = false;
const fetchData = async () => {
try {
const response = await fetch(MOVIE_DB_GET);
const result = await response.json();
console.log(result, 'result')
// непосредственное обновление состояния при условии, что компонент не размонтирован
if(!cleanupFunction) setMovie(result);
} catch (e) {
console.error(e.message)
}
};
fetchData();
// функция очистки useEffect
return () => cleanupFunction = true;
}, []);
// descriptionMovie - некая функция, возвращающая view, то есть рендерит информацию о фильме посредством состояния movie
return (
<main>
<h2>Главная страница</h2>
<p>Описание фильма</p>
{
movie ? descriptionMovie() : false
}
</main>
)
};
export default HomePage;
Итого:
- Особенности размонтирования компонента и обновление состояния
- Взаимодействие конструкции try/catch и асинхронность async/await
Весь исходный код можно посмотреть здесь: https://gitlab.com/ImaGadzhiev/react-cant-perform
MaZaAa
Проблема актуальна только лишь когда ты работаешь с локальным состоянием компонента, если используешь MobX или Redux и там делаешь асинхронные вызовы и изменение стейта, то эта проблема не актуальна.
Я думаю следует на это акцентировать внимание, чтобы паники ни у кого не возникло)
funca
Есть некая штука, которая инициирует обработку данных, которые уже ни кому не нужны. Это следствие ошибки в кодинге, приводящей к утечкам памяти или потреблению лишних ресурсов. Реакт в некоторых случаях подсказывает о проблемных местах и спасибо ему за это. Сохранение данных в глобальный стор не устраняет такие ошибки, а лишь усложняет их диагностику.
MaZaAa
1) Вэб браузер и вэб приложение на реакте и т.п. это вообще не про экономию ресурсов и памяти.
2) Сами по себе браузеры текут по памяти, вы можете это легко проследить в диспетчере задач. (Пока я писал этот комментарий и больше ничего не делал хром стал кушать + 70 Мб оперативки с того момента, как я открыл диспетчер задач, а открыл я его когда дошел до этого пункта)
3) Если данные «которые уже ни кому не нужны» в текущий момент времени, то они могут быть нужны через минуту или через секунду. Есть разные кейсы, в том числе и кэширование на клиенте ответов от сервера на несколько минут. И пользователю для этого уже будет не обязательно пялится в лоадер.
4) Даже если вы будете перед каждым сохранением данных от АПИ проверять, а есть ли сейчас экраны и компоненты активные которым эти данные нужны или нет:
— Ваш код значительно усложнится и засорится, появится куча возможностей для потенциальных багов, которые вы как раз потом задолбаетесь диагностировать.
— Ваша экономия в пару мегабайт памяти, это просто капля в море и не стоит того того, что ваш код превратится в нечто ужасное и потенциально более забагованное. Как мы все знаем, чем проще код и алгоритм, тем он работает надежнее и отказоутсойчивее.
funca
Программирование это и в самом деле борьба со сложностью. В примере из статьи сложность обусловлена тем, что вызовы await нельзя отменить, прервав выполнение функции, даже если результат нас больше не интересует. Решение задачки «в лоб», расстановкой флажков, может прилично усложнить код. Что в принципе и наблюдаем.
При том, что не нужный нам запрос все равно будет выполнен, создавая нагрузку на сервер и выкачивая лишний трафик. themoviedb штука чужая и бесплатная, но когда речь зайдет про собственный модный бекенд в клауде, то этот незначительный нюанс будет стоить вам вполне конкретных денег. Использование AbortController, как предложено ниже, решит эту проблему, но добавит еще писанины.
Если копнуть глубже, то причина сложности кроется в том, что для эффектов мы часто пытаемся использовать привычные структуры, которые для этого не особо предназначены. Promise в JavaScript является абстракцией над значением (value) — оно либо есть сейчас, либо, может быть, появится потом, — а не процессом получения этого значения. Обещание можно дать, выполнить или зафейлить. Но отменить — нельзя.
Эффекты удобнее строить с помощью того, что можно естественным образом отменять. Например — Observable:
В момент подписки (вызове subscribe()) будет инициирован запрос. Но если компонент начнет размонтироваться, то unsubscribe() все отменит.
MaZaAa
Ещё раз повторюсь
Далее, открою секрет, когда вы отправили запрос на сервер и потом не дожидаясь ответа отменили его, то все равно сервер уже иницировал тяжелые запросы в базу данных(выполнение которых нельзя отменить, они работают синхронно и не проверяют на каждый тик продолжать или нет), запросы в микросервисы которые выполняют тяжелые вычисления и т.п., всё это уже займет процессорное время на выполнение операций и не важно, нужны ли вам эти данные или уже не нужны.
Так что не стоит питать иллюзий что вы таким образом облегчите жизнь серверу, максимум что вы сделаете — сэкономите немножко трафика на клиенте, но опять же это не оправдано из-за усложнения кодовой базы и провокации потенциальных ошибок на проверки актуальности, а всё это в добавок отнимает дополнительное время и ресурсы на разработку фич и фиксы багов, ресурсы на тестирование сие чуда, менеджмент и т.д и т.п.
funca
Спасибо, что потратили время на комментарий.
MaZaAa
Я не просто потратил время, я развеял мифы вокруг тех, кто думает что отменять запросы это прям очень важно и имеет смысл