26 окт. 2025 г.·5 мин. чтения

Исправление бесконечных циклов перерендера в React: подводные камни кода, сгенерированного ИИ

Научитесь исправлять бесконечные циклы перерендера в React: находите ловушки в коде, сгенерированном ИИ, и следуйте пошаговому плану для стабильных обновлений.

Исправление бесконечных циклов перерендера в React: подводные камни кода, сгенерированного ИИ

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

Бесконечный цикл перерендера происходит, когда React-компонент рендерится снова и снова и никогда не останавливается. Вместо обычного цикла рендера что‑то постоянно инициирует новое обновление.

Первые признаки обычно заметны пользователем: страница зависает, кнопки перестают отвечать, ввод тормозит, или интерфейс мерцает, потому что состояние меняется быстрее, чем браузер успевает перерисовать.

Распространённые технические симптомы:

  • Ошибка в оверлее вроде "Too many re-renders. React limits the number of renders to prevent an infinite loop"
  • Всплески загрузки CPU, вкладка становится горячей или медленной
  • Один и тот же сетевой запрос отправляется многократно
  • Консольные логи печатаются без остановки
  • UI самосбрасывается (например, модальное окно тут же открывается снова после закрытия)

Эти циклы — не просто проблема производительности. Они ломают логику. Форма никогда не заканчивает валидацию, проверки авторизации прыгают между "вошёл" и "вышел", а эффекты, которые должны сработать один раз, выполняются сотни раз. Это часто приводит к дополнительным багам: дублированной аналитике, повторяющимся уведомлениям или проблемам с лимитами API.

Типичный сценарий: дашборд загружается, делает повторный фетч, сохраняет ответ в состояние, затем снова фетчит, потому что эффект зависит от чего‑то, что меняется на каждом рендере. Пользователь видит вечный спиннер, а бекенд — поток одинаковых запросов.

Почему код, сгенерированный ИИ, часто зацикливается

Инструменты ИИ могут сгенерировать React‑код, который выглядит корректно в демо, но разваливается при реальных данных и пользователях. Одна из причин — смешивание трёх вещей, которые должны оставаться разделёнными:

  • Источник состояния (что вы храните)
  • Производные значения (что вы вычисляете из сохранённых значений)
  • Побочные эффекты (что вы делаете, когда что‑то меняется)

Когда это смешивается, вы получаете вызовы setState во время рендера или эффекты, которые меняют те же значения, из‑за которых они запускаются снова.

Шаблон 1: «Сделать всё в одном useEffect»

В компонентах, сгенерированных ИИ, часто в одном эффекте собирают фетчинг, парсинг, фильтрацию и состояние UI. Эффект срабатывает, вызывает несколько setState, происходит ререндер, и эффект снова запускается из‑за изменившейся зависимости.

Обычно цикл выглядит так: fetch → setState → re-render → effect runs again → fetch again.

Шаблон 2: Нестабильные зависимости, созданные на каждом рендере

Ещё одна частая причина — значения в массиве зависимостей, которые пересоздаются на каждом рендере, даже если по смыслу они «те же». Это inline объекты/массивы, inline функции или производные значения, перестраиваемые при каждом рендере.

React сравнивает зависимости по ссылке, а не по глубине. Новая ссылка на объект означает «изменилось», поэтому эффект срабатывает снова.

Один реалистичный пример: дашборд формирует queryParams как объект внутри компонента и использует его в useEffect для фетча. Каждый рендер создаёт новый объект, эффект снова выполняется, фетчит, вызывает setState — и дашборд никогда не успокаивается.

Как подтвердить, где начинается цикл

Сначала убедитесь, что это реальный цикл, а не просто «много рендеров». Добавьте счётчик в начале функции компонента:

console.count('Component render')

Если счётчик растёт даже когда вы не взаимодействуете со страницей, значит цикл подтверждён.

Дальше изолируйте триггер. Временно отключайте побочные эффекты, пока цикл не остановится. Быстрее всего комментировать useEffect по одному (или возвращать ранний выход внутри них) и перезагружать после каждого изменения. Когда цикл остановится, последнее изменение укажет на источник.

Практический порядок проверки:

  • Блоки useEffect, которые вызывают setState или записывают в хранилище
  • Эффекты, делающие API‑вызовы или подписывающиеся на что‑то
  • Производные состояния, вычисляемые или синхронизируемые каждый рендер
  • Провайдеры контекста и родительские компоненты

React DevTools тоже помогают: включите подсветку обновлений и смотрите, какая часть UI мигает постоянно. Это часто показывает, в каком компоненте или на уровне выше находится цикл.

Если во вкладке Network вы видите один и тот же запрос многократно, цепочка часто такая: render → effect → fetch → setState → render. Исправление цикла предотвращает и дублирование запросов, и проблемы с лимитами.

Пошаговый план, чтобы остановить цикл

Относитесь к циклу перерендера как к цепной реакции. Одно обновление вызывает следующий рендер, который вызывает следующее обновление. Ваша задача — найти первую домино.

  1. Определите обновление, которое происходит прямо перед следующим рендером.

Смотрите, какой сеттер запускается (setUser, setItems, setLoading) и что его триггерит. Если сеттеров несколько, комментируйте их по одному, чтобы понять, какой останавливает цикл.

  1. Убедитесь, что вы не обновляете состояние во время рендера.

Частая ошибка — вызывать сеттер при «вычислении» значений или внутри хелпера, который выполняется во время построения JSX. Обновления состояния должны происходить в обработчиках событий, эффектах или коллбэках, а не в теле рендера.

  1. Сократите эффект до его реальной задачи.

Уменьшите эффект до минимальной версии, которая всё ещё воспроизводит баг. Выпишите, от чего он действительно зависит (props, state и внешние значения). Часто проблемы с зависимостями прячутся здесь.

  1. Стабилизируйте входы и сделайте обновления идемпотентными.

Небольшие исправления, которые быстро останавливают циклы:

  • Сделайте нестабильные входы стабильными (мемоизируйте коллбэки/объекты или вынесите их за пределы компонента)
  • Не храните производные значения в состоянии, если в этом нет необходимости (вычисляйте их из props/state или используйте useMemo)
  • Защищайте обновления: вызывайте setState только когда значение меняется по сути
  • Добавьте очистку для подписок, таймеров и незавершённых запросов

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

Исправление проблем с зависимостями useEffect

Остановить бесконечный цикл перерендера
Мы исправляем сгенерированный ИИ код React, который зацикливается, делает лишние запросы и непредсказуемо сбрасывает состояние.

Большинство циклов с useEffect сводится к одной схеме: эффект вызывает setState, и это изменение состояния снова вызывает эффект.

Отнеситесь с подозрением к каждому setState внутри эффекта, пока не сможете объяснить, почему он не будет повторно запускаться.

Ключевое правило: не вызывайте setState без условий внутри эффекта. Если эффект выполняется при монтировании и при изменении зависимостей, нужна проверка, которая предотвратит повторяющиеся обновления.

Практическая защита — «обновлять только когда новое значение действительно отличается». Это важно, когда код перестраивает массивы или объекты и сохраняет их в состоянии каждый раз, даже если содержимое не изменилось.

Хорошие исправления:

  • Сравнивать перед вызовом setState (или сравнивать внутри функционального обновления)
  • Мемоизировать зависимости, когда действительно нужно зависеть от объектов/функций
  • Вычислять производные значения в рендере вместо синхронизации через эффект
  • Держать зависимости преднамеренными (по возможности зависеть от примитивов)

Не рассматривайте массив зависимостей как чеклист. Линтеры полезны, но добавление зависимости, чтобы скрыть предупреждение, может превратить однократный эффект в самозапускающийся цикл. Часто реальное решение — реструктуризация: разбить эффект на два, или вынести работу в обработчик события.

Обновления состояния, случайно запускающие ререндеры

Легко зациклиться на useEffect, но некоторые циклы происходят из простых ошибок со состоянием.

Вызов setState во время рендера

Если вы вызываете setState в теле рендера, у React не остаётся выбора, кроме как рендерить снова. Это может быть прямо (обычный setX(...)) или косвенно (хелпер, который вы вызываете из JSX и который обновляет состояние). Даже то, что выглядит безобидно — например, "нормализовать данные, если чего‑то не хватает" — может превратиться в цикл.

Зеркальное состояние (derived state, которое гонится за props)

Ещё один капкан — копировать props в state и затем "синхронизировать" их при несоответствии. Если prop — новый объект на каждом рендере, логика синхронизации никогда не остановится.

Паттерны, часто вызывающие повторные ререндеры:

  • Обновление состояния во время рендера (включая хелперы, вызываемые из JSX)
  • Хранение производных значений в состоянии вместо их вычисления
  • Создание новых массивов/объектов и установка их в состояние на каждом рендере без проверки равенства
  • Передача изменяющегося пропа key, который заставляет ремонт компонента

Если следующее состояние зависит от предыдущего, используйте функциональные обновления. Вместо setCount(count + 1) пишите setCount(c => c + 1). Это избегает устаревших значений, которые могут вызывать «коррекционные» обновления.

Если у вас несколько связанных обновлений, которые отталкиваются друг от друга (загрузка, ошибки, повторы, кешированные данные), useReducer помогает держать переходы в одном месте.

Ловушки при фетчинге данных, подписках и очистке

Циклы часто прячутся внутри «обычных» побочных эффектов: фетчи, подписки и таймеры. Если каждый коллбэк вызывает setState, вы можете получить постоянные ререндеры, даже если UI визуально не меняется.

Фетчи, которые продолжают перезапускаться

Если фетч зависит от состояния, которое сам же и обновляет, вы получаете цикл.

Делайте запросы отменяемыми, чтобы устаревшие ответы не перезаписывали более свежие данные. Используйте AbortController внутри эффекта и отменяйте запрос в cleanup.

Чтобы уменьшить дубли, добавляйте простую защиту через ref (не через state), например, отслеживание ключа текущего запроса.

Подписки, таймеры и очистка

Слушатели и таймеры будут срабатывать вечно, если забыть очистку. Простое правило: каждый "start" требует соответствующего "stop" в функции очистки.

Очищайте интервалы/таймауты, отписывайтесь от слушателей и удаляйте обработчики событий.

Один источник данных — один писатель

Избегайте обновления одного и того же куска состояния из нескольких мест. Если фетч устанавливает profile, подписка тоже устанавливает profile, а ещё один эффект «синхронизирует» profile, вы создаёте обратную связь. Выберите одного владельца для записи; остальные пусть инициируют обновление/рендер, но не перезаписывают одно и то же состояние.

Частые ошибки, которые удерживают цикл живым

Диагностировать ваши хуки и эффекты
Получите чёткий разбор нестабильных зависимостей, небезопасных эффектов и записей состояния во время рендера.

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

StrictMode делает небезопасные эффекты очевидными

Если эффект выполняется дважды в разработке, React StrictMode делает своё дело. Не выключайте StrictMode, чтобы скрыть проблему. Сделайте эффект безопасным для повторных запусков: добавьте очистку, защиту обновлений или вынесите однократную инициализацию из эффекта.

Устаревшие замыкания создают «компенсирующие» обновления

Распространённый паттерн — эффект читает старое состояние, затем «исправляет» его через setState. Эта коррекция вызывает новый рендер, который снова читает устаревшее значение, и цикл повторяется.

Если эффект использует состояние, но список зависимостей не соответствует, вы можете бороться с собственными старыми значениями. Когда нужен актуальный доступ к значению, предпочитайте функциональные обновления.

Мемоизация тоже может сыграть злую шутку. useCallback/useMemo с неправильными зависимостями всё ещё создают новую функцию/объект каждый рендер. Если это значение окажется в массиве зависимостей, эффект будет срабатывать постоянно.

Быстрые способы понять, что цикл всё ещё жив:

  • Логируйте "стабильные" зависимости (функции, объекты, массивы) и смотрите, меняются ли они на каждом рендере
  • Вычисляйте производные объекты внутри эффекта из примитивных зависимостей
  • Убедитесь, что у каждой подписки/слушателя есть cleanup
  • Избегайте синхронизации state с props без ясной причины

Быстрый чеклист перед релизом

Сделайте последний проход, думая о поведении реальных пользователей. Циклы часто исчезают на «счастливом пути», но возвращаются, когда пользователи кликают быстро, переключают вкладки или у них медленная сеть.

  • Просканируйте код на предмет вызовов сеттеров, которые могут выполняться во время рендера (включая хелперы, вызываемые из JSX)
  • Читайте каждый useEffect как предложение: «Когда X меняется, сделать Y». Убедитесь, что есть условие остановки
  • Проверьте стабильность зависимостей (inline объекты, массивы и функции меняются каждый рендер)
  • Проверьте очистку для таймеров, слушателей, подписок и наблюдателей
  • Сделайте сетевые операции устойчивыми: отменяйте устаревшие запросы, дедуплируйте вызовы и игнорируйте поздние ответы

Простой тест: откройте страницу и поменяйте фильтр три раза быстро. Если увидите пересекающиеся запросы и UI постоянно «подправляет» себя, вероятно, у вас нестабильные зависимости и нет отмены запросов.

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

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

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

Баг

Паттерн обычно начинается с эффекта, который зависит от inline‑объекта. Этот объект пересоздаётся на каждом рендере, поэтому React считает, что он изменился.

// Problem
function AdminUsers({ orgId }) {
  const [users, setUsers] = React.useState([]);

  const options = { method: "GET", headers: { "x-org": orgId } }; // new each render

  React.useEffect(() => {
    fetch("/api/users", options)
      .then(r => r.json())
      .then(data => setUsers(data));
  }, [options]);

  return <UsersTable users={users} />;
}

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

Исправление

Стабилизируйте входы эффекта, избегайте ненужных обновлений и отменяйте незавершённые запросы.

function AdminUsers({ orgId }) {
  const [users, setUsers] = React.useState([]);

  const options = React.useMemo(
    () => ({ method: "GET", headers: { "x-org": orgId } }),
    [orgId]
  );

  React.useEffect(() => {
    const controller = new AbortController();

    fetch("/api/users", { ...options, signal: controller.signal })
      .then(r => r.json())
      .then(data => {
        setUsers(prev => (sameUsers(prev, data) ? prev : data));
      })
      .catch(err => {
        if (err.name !== "AbortError") throw err;
      });

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

  return <UsersTable users={users} />;
}

Чтобы проверить, что всё починилось:

  • Добавьте счётчик рендеров и убедитесь, что он перестал расти
  • Посмотрите вкладку Network: повторные запросы должны прекратиться
  • Измените orgId один раз и убедитесь, что выполняется ровно один новый фетч

Небольшой рефактор помогает не допустить возврата бага: вынесите фетч в хук useUsers(orgId), ясно называйте мемоизированные значения и держите зависимости эффектов короткими и стабильными.

Что делать, если код всё ещё зацикливается

Если вы поправили очевидную проблему (например, отсутствующую зависимость) и приложение всё ещё вертится, предположите, что триггеров больше одного. Многие циклы — это цепочка: одно обновление состояния вызывает эффект, этот эффект обновляет что‑то ещё, и другой компонент реагирует, записывая обратно в первое состояние.

Небольшого исправления достаточно, когда можно указать на одну явную причину: эффект, который на каждом запуске вызывает setState, или callback‑prop, который меняет свою идентичность каждый рендер.

Рефактор часто лучший выбор, когда компонент делает слишком много: фетчинг, сортировка, фильтрация, состояние формы и состояние UI смешаны вместе. Если вы продолжаете добавлять флаги "запустить только один раз", вы лечите симптомы.

Если вы унаследовали кодовую базу, сгенерированную ИИ, которая не успокаивается, FixMyMess (fixmymess.ai) может помочь, проследив цепочку триггеров и восстановив правильный поток состояния и эффектов, а не просто добавляя защитные флаги. Часто бесплатный аудит кода достаточно, чтобы точно указать место цикла и самое быстрое безопасное исправление.