open atlas
↑ К треку
React с нуля до senior RCT · 03 · 01

Запросы в эффектах: гонки, водопады и зачем знать базовый вариант

Запрос в useEffect — честная база: эффект по параметрам, три состояния boilerplate и две структурные ошибки — гонки, потому что ответы приходят не по порядку, и водопады, потому что дерево компонентов сериализует запросы. Каждая библиотека данных чинит именно это.

RCT Middle ◷ 17 min
Уровень
ОсновыJuniorMiddleSenior
Уже знаешь этот юнит? Пройди быструю проверку за минуту →

Баг-репорт пришёл со скринкастом: пользователь набирает «warsaw» в поиске рейсов, правильный список мигает на полсекунды, затем страница молча подменяет его рейсами по запросу «wa» — все города штата Вашингтон. Ни ошибки, ни падения, и воспроизводилось примерно раз из десяти, всегда на офисном Wi-Fi и никогда на оптике у разработчика. Запрос жил в useEffect с зависимостью от строки поиска — ровно как в туториале. Чего туториал не показал: запрос «wa» попал в холодный кеш и шёл 900 мс, запрос «warsaw» — в тёплый и вернулся за 120 мс, а setResults(data) в эффекте радостно коммитил тот ответ, который пришёл последним. Сеть переставила ответы местами, а компонент понятия не имел, на какой вопрос отвечает каждый из них. Лечение заняло четыре строки — флаг ignore в cleanup эффекта — и остаток спринта команда искала ту же гонку ещё в одиннадцати компонентах. Запросы в эффектах — не ошибка; это базовый уровень. Но у базы два структурных режима отказа, и сейчас вы встретите оба.

Честная база: эффект, параметр, три состояния

Запрос в эффекте — это синхронизация: «пока компонент показывает запрос q, на экране должен быть ответ сервера для q». Эффект перезапускается при каждом изменении query, и вы вручную пишете три состояния, нужные любому UI данных, — загрузку, ошибку, данные:

function FlightSearch({ query }) {
  const [results, setResults] = useState(null);
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    let ignore = false;                    // защита от гонки
    const controller = new AbortController(); // отмена на сети
    setLoading(true);
    setError(null);

    fetch(`/api/flights?q=${encodeURIComponent(query)}`, {
      signal: controller.signal,
    })
      .then((res) => {
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        return res.json();
      })
      .then((data) => {
        if (!ignore) { setResults(data); setLoading(false); }
      })
      .catch((err) => {
        if (err.name === "AbortError") return; // ожидаемо при cleanup
        if (!ignore) { setError(err); setLoading(false); }
      });

    return () => {            // выполняется перед перезапуском и при размонтировании
      ignore = true;
      controller.abort();
    };
  }, [query]);
  // рендер loading / error / results...
}

Это примерно двадцать строк на эндпоинт, и каждая строка заслужила своё место. Форма, которую стоит усвоить: тело эффекта запускает запрос, привязанный к одному значению query; cleanup отрекается от него. React гарантирует: cleanup выполняется перед следующим телом эффекта и при размонтировании — на этом порядке держится вся корректность.

Гонка: ответы приходят не в том порядке, в каком вы их послали

Наберите быстро «w», «wa», «war» — и вы выпустили три запроса. HTTP не гарантирует порядок между запросами: кеши, ретраи и нагрузка на сервер переставляют их свободно. Без защиты каждый ответ вызывает setResults, и последним рисуется самый медленный запрос. Две защиты делают разную работу. Флаг ignore — это корректность: переменная замыкания на каждый запуск эффекта; когда cleanup её переключает, setResults устаревшего замыкания становится no-op. Ответ всё равно скачивается — вы просто отказываетесь его коммитить. AbortController — это эффективность: он отменяет саму сетевую работу, освобождая слоты соединений браузера на источник (HTTP/1.1 даёт 6) и время сервера. Одного abort почти хватает — отклонённый promise выходит через AbortError, — но флаг ignore защищает ещё и неотменяемую асинхронную работу после fetch (уже запущенный res.json(), цепочку трансформаций), поэтому идиома — пара. StrictMode прогоняет setup→cleanup→setup при dev-монтировании ровно затем, чтобы доказать: ваш cleanup действительно отрекается от первого запроса. Если в dev видны двойные коммиты — продакшен-гонка реальна.

Викторина

Эффект поиска использует только AbortController, без флага ignore. query меняется с "wa" на "warsaw"; cleanup отменяет первый запрос. Гонка закрыта полностью?

Водопады: дерево компонентов становится расписанием сети

Вторая структурная проблема невидима внутри отдельного компонента. ProfilePage запрашивает пользователя, рендерит <Posts userId={...}> только когда пользователь пришёл, а Posts запрашивает посты и рендерит <Comments>, который запрашивает снова. Каждый fetch стартует только после того, как данные родителя приземлились и ребёнок смонтировался, — три логически независимых запроса идут последовательно. При 200 мс на круг это 600 мс чистой задержки, навязанной структурой, ещё до серверного времени; на LTE с RTT 75 мс и TLS-рукопожатием реальные трейсы регулярно показывают водопады в 1,5–2 с на страницах, которые могли бы загрузиться за один круг. Сигнатура в network-панели безошибочна: лесенка. Дерево решило ваше расписание, а дерево ничего не знает о сети. Лекарства по нарастающей амбициозности: поднять запросы в самый верхний компонент, знающий все параметры, и запустить их через Promise.all; стартовать запросы до рендера (в лоадере роута или обработчике события) и передавать promise вниз; либо отдать проблему библиотеке или фреймворку, чей кеш разделяет «кто это рендерит» и «кто это запрашивает». Последний ход — тема остатка этого юнита.

Викторина

ProfilePage запрашивает пользователя, затем рендерит Posts, который запрашивает посты, который рендерит Comments с ещё одним запросом. RTT — 200 мс. Какова минимальная навязанная структурой задержка до начала загрузки комментариев и почему?

Вспомните перед уходом
  1. 01
    Объясните гонку fetch-в-эффекте механически: откуда берётся устаревшая запись и что именно дают флаг ignore и AbortController по отдельности?
  2. 02
    Страница показывает лесенку из 4 ступеней в network-панели. Почему запросы в эффектах дают такую форму, сколько это стоит и какие три лекарства по нарастающей?
Итог

Запрос в useEffect — базовый паттерн, относительно которого меряется каждое улучшение: эффект с зависимостью от параметров запроса стартует fetch, рукописные состояния loading/error/data закрывают три момента UI, а cleanup — гарантированно выполняющийся перед следующим телом эффекта и при размонтировании — место, где живёт корректность. Первая структурная ошибка — гонка: HTTP-ответы приходят в любом порядке, и последним рисуется самый медленный запрос, если каждый запуск эффекта не отрекается от своего запроса в cleanup; флаг ignore превращает setState устаревшего замыкания в no-op (покрывая даже неотменяемые шаги вроде идущего res.json()), а AbortController отменяет реальную сетевую работу и освобождает слоты соединений — продакшен-код использует пару, а dev-цикл StrictMode setup→cleanup→setup дымово тестирует защиту. Вторая структурная ошибка — водопад: компоненты, запрашивающие при монтировании, стартуют только после коммита данных родителей, и логически параллельные запросы сериализуются вниз по дереву — три уровня при RTT 200 мс дают 600 мс задержки, которую network-панель рисует лесенкой. Подъём запросов к общему предку с Promise.all, старт запросов до рендера или кеш, отделяющий загрузку от рендеринга, — лекарства по нарастающей. Двадцать строк boilerplate на эндпоинт и два системных режима отказа — вот честная цена, которую инструменты остатка юнита существуют, чтобы вернуть. Теперь, когда встретишь баг с устаревшими данными или лесенку в network-панели, сразу знаешь, за что хвататься: за cleanup-отречение при гонках и за подъём запросов или кеш при водопадах.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 7 завершено
Связанные уроки

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.