Шэсьць памылак у асінхронным таймінгу, які выражаюцца як багі у рэндарынгу React
Выучыце, як выявляць шэсць патэранаў асінхроннага JavaScript, начыннаючы з застарэлых значэнняў, зафіксаваных у функцыях, і заканчваючы відсутнасцю рэтарнуў Promise, якія прычыняюць тое, што компаненты React паказваюць некоректны або немагчымы стан.
Большая канцэлка прыбліжэнняў, якія фіксуюцца як „багі React“, ніколі не выходзяць з самога React. Спіс, які паказвае рэзультаты старэйшага запиту, індыкатор загрузкі, які ніколі не зникае, паведамленне пра успех, яке аплывае раней, чым данні готавы: React — это проста тое месца, дзе этыя проблемы становяцца виднымі. Асалодзічная прычына зазвычай знаходзіцца на адны ўрадок нижэй, у тым, як асінхронны JavaScript перадае значэнні з часам.
Асінхронны код вносіць час у програму, і кожна оператыя await або .then() — это момент, калі ўсё пад вамі можа змяніцца. Ёсць шасць часта павтараючыхся помылак у рэгулюванні часу, прычыны таго, чаму кожная з іх вызывае плутаныя сімптамы інтерфейса, а таксама незначныя змены, якія іх вылечваюць, ўпрынцыпе, каб вас можна было выявіць пад час перагляду коду.
Памылка 1: Застарэлыя запиты ўсё равно збіраюць данні для текущага стану
Поле пошуку — это класычны прыклад. Наведзены выжыўшы код вже затрымвае введэнне на 300 мс і абрывае таймер пад час чысткі.
useEffect(() => {
if (!query) {
setResults([]);
return;
}
const timeoutId = setTimeout(async () => {
const results = await search(query);
setResults(results);
}, 300);
return () => {
clearTimeout(timeoutId);
};
}, [query]);
Уявіце, калі хтось пачынае пісаць „Async JavaScript Mistakes“. Яны пішу „Async“, чакаю достатньо часу, каб функцыя debounce завершылася, і надаецася запит. Потым яны продовжую пісаць, і надаецаецца ўжо другі запит з усім фразам. Функцыя debounce зменшыла колькість запитоў, але не запобегла тому, каб два з іх не надходзілі одночасна.
Парадакт, у якім запиты выходзяць з браузера, належыць да вашага кантролю. Парадакт, у якім прыходзяць адпаведзі, — ні. Якщо дыялог з болей дзьвігучай назвой завершыцца першы, а дыялог з „Async“ — вось-такі, то другі вызов setResults будзе перамагаць, і спіс пакажае рэзультаты для „Async“, хоця у введанні чытаецца ясна фраза „Async JavaScript Mistakes“.
Жадна проблема тут няма. Сеткі з’еднанняў могу пераранжаваць рэзультаты, а код ніколі не паведамляў React, якай рэспонс ёшча є актуальным. Звычны спосаб — анулюваць непатрэбную работу за дапамою AbortController, створанага ўнутрь эфекту, і анулюванага пад час його чысткі, так што застарэлы рэспонс або ніколи не прыходзіць, або ігнаруецца. Якщо поток дадзеных вже пабудаваны на RxJS, switchMap дае тую ж семантыку „толькі найсвежэйшыя данні маюць прывілеў“. Для болей шырокага розгляду гэтага сценарыю, адзірніце способы выправлення ситуацый, якія не можна рашыць за дапамою тэхнікы debouncing.
Памылка 2: Рашэнне на аднойчы зафіксаваных значэннях пры выкарыстоўванні await
Спрыткуйце кожны await як межу. Усё, што вы зналі да гэтага моменту, — це кароткі фатаг стану; усё, што выходзіць пасля яго, выконваецца пазней, можа пасля таго, як змяніўся іншы стан.
const handlePublish = async () => {
const { canPublish } = permissions;
await saveDraft();
if (canPublish) {
publish();
}
};
Гэты працоўнік чытае значэнне canPublish з масіва permissions, чакае, пакуль проект будзе захаваны, а потым вырашае, чы розпрысціваць яго. Проблема заключаецца ў тым, што рашэнне базуецца на значэнні, якое было правдзівым у пачатку. Якщо права корыстніка былі анульваны пад час выконання функцыі saveDraft(), локальная сталяя яшчэ і даўайце „так“.
Тая ж ситуацыя выклікаецца з выбранымі элементамі, актыўнымі фільтрамі, параметрамі маршрута, зместам рэдаггера і багатымі іншымі элементамі стану. Калі рашэнне пасля await залежыць ад текущага стану прыкладнення, перачытайце гэты стан пасля межы (з рэферэнса, хранілнікай або новага запиту) у замест на тое, каб паверыць ранейшай копіі.
Памылка 3: Прыязджанне, што рэштка функцыі завучваецца ўсё
Ёсць флаг загрузкі, які выкарыстоўваецца разам з методам fetch.
setLoading(true);
const dashboard = await getDashboard();
setDashboard(dashboard);
setLoading(false);
Якщо getDashboard() вярнёў адмову, выконанне пераходзіць за межы функцыі на рэгламент await, і setLoading(false) ніколі не выкарыстоўваецца. Індыкатор загрузкі застаёцца на экране няскончана.
Калі які-небудзь элемент стану моделюе трымачасовасць асінхронной операцыі, яго перазначэнне павінна выконвацца як у разе успеху, так і ў разе адмовы. Блок finally працоўна выражае гэтую намеру:
setLoading(true);
try {
const dashboard = await getDashboard();
setDashboard(dashboard);
} finally {
setLoading(false);
}
Заўважыце, што блак try яшчэ не мае блака catch. Адмова продавягваецца да таго, хто вызваў гэты код, што часта і є бажаным, тады як флаг загрузкі гарантавана будзе асцярожаны. Дадайце блак catch толькі тады, калі гэта правильнае месца для ператварэння адмовы ў элемент інтэрфейсу, напрыклад у паведамленне пра адмову.
Памылка 4: Незалежныя запросы, які ствараюць несупарадныя станы экрана
Адправка незв’язанных запросаў паралельна часта ёсць абсолютна разумным крокам.
getProfile().then(setProfile);
getPermissions().then(setPermissions);
getPreferences().then(setPreferences);
Кожна адпаведь падае у свой сабэй стану, калі толькі прыходзіць. Гэта значыць, што React можа адрасаваць любую комбінацыю: прафіль без прав або настройкаў, права і настройкі без прафіля, а таксама будь-якую іншую параду, яку стварае сеть.
Дзеяныя з тымі камбінацыямі можаць быць бессэнсавымі або нават небяпечнымі для вашага экрана. Калі калькія адпаведзей разам описваюць адны цэлісны стан экрана, запыткі можуць выконваліцца паралельна, а ўсё разам складае адны власнік — напрыклад, чакаючы Promise.all і зафіксаваўшы всё ў адной змене стану, або моделюючы экран як редукера з чыста выражанымі станамі завантажэння, гатовасці і адзычыны. Паралельнае запрашэнне дадзеных і незалежны стан UI — гэта два адналежнага рашэння.
Памылка 5: Стварэнне наступнага стану з застарелага кадру
Гэтая памылка лёгкая спрываецца ў працоўнікавых функцыях.
const handleAdd = async () => {
await saveItem(newItem);
setItems([...items, newItem]);
};
items — гэта тое, што храніла компанента калі пачыналася функцыя handleAdd. Калі saveItem() была ў процесе выканання, іншая дзеянасць могла дадаць або выдаліць елементы. Калі працоўнік зноў запускаецца, ён выкарыстоўвае стары масів і перазапісывае новейшыя змены.
Калі наступная значэнне залежыць ад пярэдней, нехай React задае пярэднюе значэнне через форму апдейтара:
setItems(current => [...current, newItem]);
Апдейтар працуе з найсвежэйшым станам, які быў зафіксаваны ў момент обробкі апдэйта, таму сумаверныя змены захавляюцца. Застарэлыя клаузуры — гэта не толькі проблема эфектаў: будзь-якая асінхронная калебачка можа зберагаць значэнні дыяўольш часу, чым вы спадзяваліся.
Памылка 6: Разбіянне ланцуга Promise заўсёды не вяртаючы рэзультат
Эты ланцуг выглядае абоўсюды секвэнцыйна: запісаць, пасля чаго апавесціць віджэты, а потым пазначыць як запісаныя.
saveDashboard()
.then(() => refreshWidgets())
.then(() => setSaved(true));
Тепер паглядзіце, як можа быць напісаны refreshWidgets:
const refreshWidgets = () => {
getWidgets().then(setWidgets);
};
Функцыя пачынае запуск запиту, але вяртае undefined, а не Promise. З точкі зору зовнішняе ланцоўкі, refreshWidgets() завершылася мгновэна, таму наступны .then выконваецца адразу, і setSaved(true) можа быць выкананы, калі віджэты ўсё яшчэ завантажаюцца.
Рашэнне — адна ключоўая слова, якая вяртае Promise, ўжо каб ланцоўка могла чакаць на яго:
const refreshWidgets = () => {
return getWidgets().then(setWidgets);
};
Функцыя async неявна досягае таго ж рэзультата, адтолькі ўсега вяртае Promise, яны ўжо завершваюцца, калі выкананы ўсе ў яе тэле:
const refreshWidgets = async () => {
const widgets = await getWidgets();
setWidgets(widgets);
};
У інтерфейсе корыстніка гэты баг можа выглядаць як прытаманная паведамленне аб адразе, які з’являецца занадта рана, старыя даныя, якія застаюцца на экране, або перенаправленне, якое вырабляецца раней, чым завершыцца апдэйт. Нічога з гэтаго не ўскладнюе адразу дыягностику відсутнасці оператара return. Правіла праблейскутавання TypeScript, такія як @typescript-eslint/no-floating-promises, можа автаматычна выявіць багато з гэтых случаў.
Ключовыя выводы
Межа між React і JavaScript не завжды ўсведамлена. React атрыбутуе стан, але асінхронны JavaScript вяршыць рашэнне пра тое, калі гэты стан будзе отрыманы і чы робіцься ён яшчэ актуальным, застарелым чы несувярэнным у момент атрыбутавання. Працюючы з асінхронным кодам у компанентах, пад час аналізу большасць гэтых багоў выклікаюць тры пытанні:
- Калі была зафіксавана гэтае значэнне, і чы ў момент викорыстання
awaitяно могла змяніцца?