Галоўная / Артыкулы / Шэсьць памылак у асінхронным таймінгу, які выражаюцца як багі у рэндарынгу React

Шэсьць памылак у асінхронным таймінгу, які выражаюцца як багі у рэндарынгу React

Выучыце, як выявляць шэсць патэранаў асінхроннага JavaScript, начыннаючы з застарэлых значэнняў, зафіксаваных у функцыях, і заканчваючы відсутнасцю рэтарнуў Promise, якія прычыняюць тое, што компаненты React паказваюць некоректны або немагчымы стан.

1485 слоў

Большая канцэлка прыбліжэнняў, якія фіксуюцца як „багі 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 яно могла змяніцца?
  • Калькія асінхронная операцыя ўтварае гэту зміну стану, і чы можа старэйшая операцыя перазапісаць новейшую?
  • Чы операцыя застаецца рэлевантной пасля завершэння, і чы кожны варіант розвитку, уключаючы абяроны, заставляе інтэрфейс застацца ў правільным стане?