Инженерная практика

Запросы в React: useEffect, отмена и устаревшие ответы

Почему быстрый поиск показывает старые данные. Разбираем useEffect, AbortController, состояния запроса и проверку race condition на маленьком примере.

Представьте поиск в каталоге: пользователь ввёл «ре», затем «реакт». Второй запрос завершился первым и показал нужный список. Через секунду пришёл ответ на «ре» и затёр результат. API вернул корректные данные для каждого запроса, компонент тоже не упал. Ошибка находится в связи между текущим намерением пользователя и правом ответа менять интерфейс.

Сначала решите, нужен ли эффект

Сортировка уже полученного списка, подсчёт выбранных элементов и фильтрация локального массива обычно не требуют useEffect. Эти значения можно получить из текущих props и state. Отдельный state для их копии заставляет поддерживать два источника истины. Это усложняет смену фильтра: один рендер уже содержит новое условие, а скопированный результат ещё старый.

Запрос к серверу - другой случай. Он обращается к внешней системе и может завершиться после следующего рендера. Если источник запроса - конкретное действие, например отправка формы, обработчик события часто понятнее эффекта. Если данные должны соответствовать текущему параметру компонента, эффект демонстрирует такую синхронизацию. Ни один из подходов не отменяет необходимость правильно обработать порядок ответов.

Минимальный эффект с отменой и защитой результата

В примере URL - полный адрес GET-запроса. Сервер возвращает массив строк. Проверка Array.isArray здесь намеренно маленькая: для сложного ответа выделите отдельный парсер. AbortController останавливает fetch, а active запрещает старому эффекту записывать данные или ошибку. Оба механизма относятся к одному запуску эффекта.

TSX// React: один источник состояния запроса import { useEffect, useState } from 'react' type State = | { status: 'loading' } | { status: 'success'; items: string[] } | { status: 'error'; message: string } export function useItems(url: string): State { const [state, setState] = useState<State>({ status: 'loading' }) useEffect(() => { const controller = new AbortController() let active = true setState({ status: 'loading' }) void (async () => { try { const response = await fetch(url, { signal: controller.signal }) if (!response.ok) throw new Error('HTTP ' + response.status) const data: unknown = await response.json() if (!Array.isArray(data) || !data.every(x => typeof x === 'string')) { throw new Error('Неверный формат ответа') } if (active) setState({ status: 'success', items: data }) } catch { if (active && !controller.signal.aborted) { setState({ status: 'error', message: 'Не удалось загрузить список' }) } } })() return () => { active = false controller.abort() } }, [url]) return state }

Cleanup сначала закрывает возможность записи, затем отменяет работу. В catch отмена не превращается в видимую ошибку: пользователь изменил запрос, а не сломал соединение. При настоящем отказе мы даём безопасное сообщение. Для диагностики приложения отдельно нужны логирование и способ повторить запрос; этот хук не является полноценной системой наблюдаемости.

Что здесь ещё важно

До выполнения нового эффекта компонент может один раз получить старое состояние для нового URL. Если показывать эти данные нельзя, сохраняйте в состоянии также ключ запроса и сопоставляйте его с текущим URL при рендере. Не скрывайте эту границу за обещанием «гонки устранены полностью». Библиотеки запросов тоже различают предыдущие и актуальные данные, но позволяют явно выбирать поведение интерфейса.

Как проверить гонку воспроизводимо

  1. Подмените сеть управляемыми ответами. Создайте запрос A, затем измените URL и создайте B.
  2. Сначала завершите B и убедитесь, что показывается его результат.
  3. Затем завершите A. На экране должны остаться данные B. Повторите сценарий с ошибкой A: она также не должна затереть успех B.
  4. Размонтируйте компонент до ответа. После этого не должно появиться новое состояние или сообщение об ошибке.
  5. Отдельно проверьте HTTP 500, неправильный JSON и пустой массив: это три разных границы, а не один тест «ошибка».

Задержка setTimeout в тесте сама по себе не гарантирует порядок. Удобнее разрешать промисы вручную: тогда тест управляет причиной ошибки. Проверяйте видимое состояние и запрос отмены; проверка количества вызовов useEffect слишком тесно связана с реализацией и режимом разработки.

Когда учебного эффекта становится мало

Вручную написанный эффект быстро обрастает кешем, повторными запросами, дедупликацией, обновлением по фокусу окна и синхронизацией нескольких экранов. На этом этапе сначала сформулируйте требования к серверным данным, затем выберите инструмент. useMemo не превращает fetch в кеш, а useCallback не отменяет устаревший ответ. Важен контракт данных, а не число использованных хуков.

Практическое задание: добавьте поиск, пустое состояние и повторную попытку. Опишите, когда пользователю показываются предыдущие данные. Проверку структуры ответа продолжите в разборе TypeScript на границе API; критерии проекта - в практикуме.

Источники и область применения

Механика эффекта и cleanup: документация React. Вычисление производных значений: You Might Not Need an Effect. Отмена fetch: AbortController в MDN. Пример рассчитан на клиентский React; особенности загрузки данных в серверных компонентах требуют отдельного решения.

Вопросы по теме

Достаточно ли AbortController для запроса в React?
Для fetch он позволяет отменить запрос. Но последующая обработка или сторонний клиент могут продолжить работу: перед записью результата также проверяйте актуальность эффекта.
Нужен ли useEffect для фильтрации массива?
Если результат полностью определяется props и state, вычислите его при рендере. Эффект для копирования производного значения в state создаёт лишнее состояние и промежуточный рендер.
Нужно ли писать собственный кеш запросов?
Небольшой эффект подходит для понимания механики. Для повторно используемых серверных данных, кеша и нескольких потребителей рассмотрите механизм фреймворка или библиотеку запросов.

Читайте также

Все статьи

Обсудим вашу подготовку?

Напишите, что уже умеете и с чем возникли трудности. Если есть проект, пришлите ссылку на него.

Отвечаю лично, поэтому прошу рассказать о ситуации подробно. Все поля обязательны, кроме отмеченных. После отправки покажу ссылку на мой Telegram: напишите туда, и начнём разговор.

Что не получается, что уже пробовали. От 100 знаков, сейчас 0.
Где работали или учились, сколько лет, какой стек, сколько откликов и собеседований. От 100 знаков, сейчас 0.
PDF или DOCX, до 5 МБ. Файл видит только администратор.
GitHub, hh, LinkedIn, сайт. Можно несколько, через пробел или с новой строки.