Сгенерированный ИИ фронтенд выглядит идеально на демо: авторизация, таблицы, формы с валидацией, тёмная тема — всё поднимается одной командой. Проблема вылезает не на демо, а когда пользователь быстро печатает в поле поиска и получает результаты не для того запроса, который набрал. Разбираю на реальном примере, какие пять мест в ИИ-коде нужно проверять руками и какими правками их закрывать за минуты, а не часы отладки.

Почему демо обманывает, а прод — нет

ИИ-модели вроде GPT или Claude отлично пишут код для «счастливого пути»: запрос ушёл, ответ пришёл, интерфейс обновился. Они хуже предугадывают состояния, в которые приложение попадает само — сеть отвалилась, пользователь кликнул дважды, ушёл со страницы на середине запроса.

Разработчик, который тестирует новый функционал по методичной схеме «ввести → отправить → проверить результат», такие баги пропускает. Их находят либо реальные пользователи в проде, либо целенаправленная проверка граничных сценариев — отключить сеть в devtools, быстро напечатать текст, кликнуть кнопку дважды подряд.

Гонка запросов в поиске: находим и чиним

Классический баг в сгенерированном поиске выглядит так:

typescript
function ExpertSearch() {
  const [query, setQuery] = useState("");
  const [experts, setExperts] = useState<Expert[]>([]);
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    if (!query) return;
    setIsLoading(true);
    fetch(`/api/experts?q=${query}`)
      .then((r) => r.json())
      .then((data) => setExperts(data.items))
      .catch(() => setExperts([]))
      .finally(() => setIsLoading(false));
  }, [query]);
}

Пользователь печатает «мар», «марк», «марке» — уходят три запроса. Ответы приходят в порядке ответа сервера, а не в порядке набора текста. Если ответ на «мар» задержался и пришёл последним, он перезапишет результаты для «марке». В поле — одно, в списке — другое.

Проверка занимает минуту: откройте вкладку Network в devtools, включите throttling на Slow 3G и быстро напечатайте несколько символов подряд. Если список результатов дёргается или показывает не то — гонка есть.

Правильное решение — не «поймать» гонку вручную, а сделать её невозможной архитектурно. React Query (пакет @tanstack/react-query) привязывает результат к ключу запроса, а не к порядку ответа:

typescript
const { data, isPending } = useQuery({
  queryKey: ["experts", query],
  queryFn: ({ signal }) => searchExperts(query, signal),
  enabled: query.length > 1,
  placeholderData: keepPreviousData,
});

Устаревший ответ просто не попадёт в стейт, потому что React Query сверяет его с актуальным queryKey. Это тот самый случай, когда правка не «чинит баг», а убирает целый класс багов.

Зависшие лоадеры: ищем try/finally без catch

Второй частый паттерн — форма, которая зависает после ошибки сети:

typescript
const handleSubmit = async (form: BookingForm) => {
  setIsSubmitting(true);
  const booking = await api.createBooking(form);
  toast.success("Бронь создана");
  setIsSubmitting(false);
  navigate(`/bookings/${booking.id}`);
};

Если createBooking бросает исключение, строка setIsSubmitting(false) никогда не выполнится. Кнопка остаётся заблокированной, спиннер крутится бесконечно, пользователь теряет заполненную форму после перезагрузки страницы.

Проверка: отключите сеть в devtools (вкладка Network → Offline) и отправьте форму. Если кнопка не разблокируется и нет сообщения об ошибке — баг найден.

Правка — вынести сброс состояния в finally и обернуть запрос в try/catch:

typescript
const handleSubmit = async (form: BookingForm) => {
  setIsSubmitting(true);
  try {
    const booking = await api.createBooking(form);
    toast.success("Бронь создана");
    navigate(`/bookings/${booking.id}`);
  } catch (error) {
    toast.error("Не удалось создать бронь, попробуйте ещё раз");
  } finally {
    setIsSubmitting(false);
  }
};

Это трёхстрочная правка, но именно её ИИ пропускает чаще всего, потому что в промпте редко явно просят «обработай неуспешный сценарий».

Отмена запросов при уходе со страницы

Третья дыра — запросы, которые продолжают жить после того, как пользователь ушёл с экрана. setExperts в примере выше вызовется, даже если компонент уже размонтирован, а при быстром наборе текста в сети одновременно висит по пять запросов.

Если используете fetch напрямую, передавайте AbortController и отменяйте его в cleanup-функции useEffect:

typescript
useEffect(() => {
  const controller = new AbortController();
  fetch(`/api/experts?q=${query}`, { signal: controller.signal })
    .then((r) => r.json())
    .then((data) => setExperts(data.items))
    .catch((err) => {
      if (err.name !== "AbortError") setExperts([]);
    });

  return () => controller.abort();
}, [query]);

В React Query это встроено — функция queryFn получает signal автоматически, и библиотека сама отменяет устаревшие запросы при смене queryKey. Ещё один аргумент в пользу того, чтобы не писать ручной fetch в useEffect, если модель предлагает такой вариант.

Чек-лист ревью перед мержем

Пять минут по этому списку экономят полдня отладки в проде.

Что проверитьКак проверитьЧем чинить
Гонка запросовБыстро напечатать текст в поиске на throttled сетиReact Query с `queryKey`, привязанным к запросу
Зависший лоадерОтключить сеть и отправить форму`try/catch/finally` вокруг асинхронного запроса
Утечка после ухода со страницыПерейти на другую страницу во время загрузки`AbortController` или встроенный `signal` React Query
Двойной кликКликнуть кнопку отправки дважды подрядДизейблить кнопку сразу, до ответа сервера
Необработанный rejectОткрыть консоль браузера при ошибке сетиЛогировать и показывать пользователю, а не молчать

Где ломается

ИИ-модели обучены на коде, где нет времени и денег объяснять граничные случаи — большинство обучающих примеров в интернете показывают «счастливый путь». Даже подробный промпт со стеком, структурой папок и конвенциями не гарантирует обработку ошибок, если явно её не запросить.

Вторая проблема — модель не видит контекста продукта. Она не знает, что для банковского приложения потерянная форма бронирования — это жалоба в поддержку, а не мелочь. Приоритет граничных случаев всё ещё расставляет человек.

Что попробовать дальше

Добавьте в системный промпт явное требование: «обрабатывай сетевые ошибки через try/catch, используй AbortController или React Query для отмены запросов, не оставляй состояние загрузки без сброса в finally». Это не устраняет проверку руками, но снижает число очевидных багов на входе.

Заведите короткий чек-лист (как таблица выше) и прогоняйте по нему каждый сгенерированный компонент с сетевыми запросами — это быстрее, чем читать код построчно в поисках гонки.

FAQ

Можно ли доверять ИИ код без ревью, если промпт был очень подробным?

Нет. Даже детальный промпт со стеком и конвенциями не гарантирует обработку граничных случаев — сеть отвалилась, пользователь кликнул дважды. Эти сценарии нужно проверять руками или тестами.

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

Включите throttling в devtools (Slow 3G) и быстро напечатайте несколько символов в поле поиска. Если результаты в списке не совпадают с текстом в поле — гонка есть.

Почему React Query лучше решает проблему гонки, чем useEffect с fetch?

React Query привязывает результат к ключу запроса (queryKey), а не к порядку ответа от сервера. Устаревший ответ просто не попадёт в состояние, даже если пришёл позже актуального.

Что делать, если проект уже использует ручной fetch в useEffect?

Добавьте AbortController и отменяйте запрос в cleanup-функции useEffect. Это не так надёжно, как React Query, но закрывает базовый случай устаревших ответов.

Заменит ли ИИ фронтенд-разработчиков в ближайшие годы?

ИИ уже хорошо генерирует структуру и стандартные паттерны, но не умеет предугадывать нестандартные состояния приложения без явного запроса. Роль разработчика смещается в сторону ревью, архитектуры и обработки граничных случаев.

Источники