Сгенерированный ИИ фронтенд выглядит идеально на демо: авторизация, таблицы, формы с валидацией, тёмная тема — всё поднимается одной командой. Проблема вылезает не на демо, а когда пользователь быстро печатает в поле поиска и получает результаты не для того запроса, который набрал. Разбираю на реальном примере, какие пять мест в ИИ-коде нужно проверять руками и какими правками их закрывать за минуты, а не часы отладки.
Почему демо обманывает, а прод — нет
ИИ-модели вроде GPT или Claude отлично пишут код для «счастливого пути»: запрос ушёл, ответ пришёл, интерфейс обновился. Они хуже предугадывают состояния, в которые приложение попадает само — сеть отвалилась, пользователь кликнул дважды, ушёл со страницы на середине запроса.
Разработчик, который тестирует новый функционал по методичной схеме «ввести → отправить → проверить результат», такие баги пропускает. Их находят либо реальные пользователи в проде, либо целенаправленная проверка граничных сценариев — отключить сеть в devtools, быстро напечатать текст, кликнуть кнопку дважды подряд.
Гонка запросов в поиске: находим и чиним
Классический баг в сгенерированном поиске выглядит так:
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) привязывает результат к ключу запроса, а не к порядку ответа:
const { data, isPending } = useQuery({
queryKey: ["experts", query],
queryFn: ({ signal }) => searchExperts(query, signal),
enabled: query.length > 1,
placeholderData: keepPreviousData,
});Устаревший ответ просто не попадёт в стейт, потому что React Query сверяет его с актуальным queryKey. Это тот самый случай, когда правка не «чинит баг», а убирает целый класс багов.
Зависшие лоадеры: ищем try/finally без catch
Второй частый паттерн — форма, которая зависает после ошибки сети:
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:
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:
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, но закрывает базовый случай устаревших ответов.
Заменит ли ИИ фронтенд-разработчиков в ближайшие годы?
ИИ уже хорошо генерирует структуру и стандартные паттерны, но не умеет предугадывать нестандартные состояния приложения без явного запроса. Роль разработчика смещается в сторону ревью, архитектуры и обработки граничных случаев.