Почему React может рендерить дважды в dev режиме: Strict Mode без мифов

Разбираем, почему React в режиме разработки может вызывать рендер и эффекты дважды, какие баги это выявляет, как отличать норму от дефекта и как отвечать на собеседовании.

13 марта 2026 г.17 минLexicon Team

Введение

Вопрос «почему React рендерит дважды» регулярно всплывает после включения Strict Mode и почти всегда вызывает одну и ту же реакцию: разработчик видит повторяющиеся записи в логах, считает это багом React и пытается устранить проблему, отключая режим.

Реальная картина сложнее. В dev режиме React действительно может повторно вызывать рендер и жизненный цикл эффекта, но это сделано как проверка устойчивости кода. Такой механизм помогает поймать дефекты раньше, чем они дойдут до пользователей.

Контекст Strict Mode удобнее читать параллельно с отдельным разбором режима и его проверок, потому что именно он определяет «двойное» поведение в разработке.

В статье разберем архитектуру процесса, типовые ловушки, разницу между dev и production, и практический шаблон ответа для собеседования.

Больше вопросов в Telegram

Ежедневные разборы и реальные кейсы с интервью

Подписаться

Что именно в React «вызывается дважды»

Фраза «React рендерит дважды» неточная. Важно разделять несколько уровней:

  1. Повторный вызов функции компонента в render phase.
  2. Повторный вызов setup-функции эффекта с промежуточным cleanup для проверки корректности (setup -> cleanup -> setup).
  3. Повторная инициализация небезопасных побочных эффектов, если код написан хрупко.

При этом двойной рендер в dev не означает двойной commit в DOM в том же смысле, как это было бы в production-инциденте. React в первую очередь проверяет, выдерживает ли компонент повторный запуск без утечек, гонок и неявных мутаций.

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

Архитектура механизма: зачем React делает это в dev

Контекст задачи

React-компоненты часто взаимодействуют с внешним миром: подписки, таймеры, fetch, аналитика и websocket-соединения. Ошибки в этих местах не всегда видны при «счастливом» сценарии, но всплывают при быстрых mount/unmount, повторной навигации и конкурентных обновлениях.

Схема компонентов и поток управления

Упрощенная схема для Strict Mode в dev:

  1. React вызывает render-функцию компонента.
  2. React коммитит изменения в дерево.
  3. useEffect выполняет setup.
  4. React сразу запускает cleanup как проверку корректности.
  5. React снова запускает setup.

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

Границы ответственности

  • React показывает проблему через dev-поведение.
  • Команда исправляет код, чтобы эффект был идемпотентным.
  • Тесты закрепляют ожидаемую семантику cleanup.

Узкие места и деградация

Критическая точка отказа: побочный эффект внутри рендера или эффект без корректного return cleanup. При росте сложности экрана это превращается в флейки, скачки состояния и спорадические баги навигации.

Связанная тема про внутреннюю архитектуру планировщика и приоритетов раскрыта в разборе React Fiber.

Код-пример 1: почему дублируется аналитика

function ProductCard({ id }: { id: string }) {
  // Ошибка: побочный эффект в render phase
  analytics.track("product_card_rendered", { id });

  return <div>Card: {id}</div>;
}

Проблема не в Strict Mode, а в месте вызова. Любой повторный рендер снова отправляет событие. В dev вы видите это сразу, в production дефект проявится менее заметно, но с искажением аналитических данных из-за многократной отправки событий.

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

function ProductCard({ id }: { id: string }) {
  useEffect(() => {
    analytics.track("product_card_viewed", { id });
  }, [id]);

  return <div>Card: {id}</div>;
}

Даже здесь нужно понимать семантику: это событие привязано к просмотру карточки, значит эффект уместен. Для события «нажал кнопку» логика должна жить в onClick, а не в useEffect.

Код-пример 2: двойной fetch в dev и корректная защита

Проблемный вариант:

function Profile({ userId }: { userId: string }) {
  const [user, setUser] = useState<User | null>(null);

  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then((r) => r.json())
      .then(setUser);
  }, [userId]);

  if (!user) return <p>Loading...</p>;
  return <p>{user.name}</p>;
}

В Strict Mode вы можете увидеть два запроса на mount. Это не повод выключать режим; это сигнал, что в эффекте нет отмены и защиты от устаревшего ответа.

Устойчивый вариант:

function Profile({ userId }: { userId: string }) {
  const [user, setUser] = useState<User | null>(null);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const controller = new AbortController();
    let isActive = true;

    fetch(`/api/users/${userId}`, { signal: controller.signal })
      .then((r) => {
        if (!r.ok) throw new Error(`HTTP ${r.status}`);
        return r.json();
      })
      .then((payload) => {
        if (isActive) setUser(payload);
      })
      .catch((e: unknown) => {
        const err = e as { name?: string; message?: string };
        if (err.name !== "AbortError" && isActive) {
          setError(err.message ?? "Unknown error");
        }
      });

    return () => {
      isActive = false;
      controller.abort();
    };
  }, [userId]);

  if (error) return <p>Ошибка: {error}</p>;
  if (!user) return <p>Loading...</p>;
  return <p>{user.name}</p>;
}

Такой шаблон устраняет гонки и делает повторный dev-запуск безопасным. Для диагностики цепочки ререндеров и эффектов удобно использовать React DevTools Profiler.

Сравнение подходов

КритерийStrict Mode включен (dev)Strict Mode выключен (dev)Production сборка
Раннее выявление неидемпотентных эффектовВысокоеНизкоеНизкое до инцидента
Шанс увидеть ошибку cleanup до релизаВысокийНизкийСлучайный
Комфорт локальной разработкиСреднийВысокий краткосрочноНе относится
Риск скрыть архитектурный дефектНизкийВысокийВысокий
Достоверность замеров runtime-производительностиНиже из-за дополнительных проверокВыше для dev-профиляРеалистичная
Польза для подготовки к интервьюВысокая (понятны причины дублей)СредняяНейтральная

Таблица показывает практический компромисс: отключение Strict Mode уменьшает шум сегодня, но переносит стоимость исправлений на более поздний этап.

Production pitfalls: где команды чаще всего ошибаются

Ошибка 1. «Починить» дубль отключением Strict Mode

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

Ошибка 2. Не делать cleanup у подписок

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

Ошибка 3. Мутировать внешнее состояние во время рендера

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

Ошибка 4. Игнорировать race condition при fetch

Симптом: старый ответ перезаписывает новый после быстрой смены userId.
Последствие: интерфейс показывает устаревшие данные.
Профилактика: AbortController и защита от неактуального ответа.

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

Strict Mode в dev часто увеличивает объем работы, поэтому увеличение времени локальной разработки само по себе не является багом. Ошибка начинается тогда, когда команда делает вывод о production-производительности по dev-симптомам.

Практический разбор:

  1. Сначала измеряйте production build в профайлере браузера.
  2. Затем отдельно анализируйте dev-шум от проверок Strict Mode.
  3. После фикса эффекта проверяйте, уменьшилось ли число лишних ререндеров в профиле.

Если компонент перегружен и часто обновляется, помогает архитектурное разделение состояния и мемоизация на границах. Детальный разбор, когда это оправдано, есть в материале про React.memo, useMemo и useCallback.

Оптимизацию стоит делать после замера. Иначе легко получить «улучшение», которое усложнило код, но не дало выигрыша по p95 времени отклика.

Прокачай React за 7 дней

20 вопросов и разборов по React Hooks

Начать

Практики, которые работают в команде

1. Проектируйте эффекты так, чтобы их можно было безопасно запускать повторно (идемпотентность)

Каждый useEffect должен корректно переживать сценарий setup -> cleanup -> setup. Если это невозможно, архитектура эффекта слишком хрупкая.

2. Следите, чтобы на этапе рендера не выполнялись побочные действия

В рендере только вычисление JSX. Любая операция с внешним миром должна быть вынесена в эффекты или обработчики.

3. Разделяйте эффекты по ответственности

Один эффект на один процесс: подписка, запрос, синхронизация URL. Смешивание нескольких процессов в одном блоке резко повышает вероятность ошибок cleanup.

4. Тестируйте быстрые mount/unmount сценарии

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

5. Фиксируйте инженерные правила в код-ревью

Чек-лист для review: есть ли cleanup, есть ли отмена запроса, есть ли побочный эффект в render phase, есть ли контроль гонок.

Частые ошибки

  • Считать, что «два рендера в dev» автоматически означают баг React.
  • Путать повторный вызов функции компонента с физическим двойным обновлением DOM.
  • Делать запросы в useEffect без отмены и защиты от устаревших ответов.
  • Отключать Strict Mode в проекте вместо точечного исправления эффекта.
  • Сравнивать производительность в dev- и production-сборках, не учитывая дополнительных проверок Strict Mode.

Как отвечать на интервью

Хороший короткий ответ:

  1. В dev React Strict Mode может повторять рендер и цикл эффекта, чтобы выявить неидемпотентные side effects.
  2. Это диагностическое поведение, не равное production-семантике.
  3. Если видите дубль запроса, нужно исправлять эффект: cleanup, abort, защита от гонок.
  4. Отключение Strict Mode скрывает симптом и повышает риск production-багов.

Сильный ответ для middle-уровня обычно дополняется объяснением компромисса: dev становится шумнее, но качество кода и предсказуемость поведения под нагрузкой растут. Полезно также упомянуть связь темы с concurrent rendering в React, потому что обе области требуют устойчивых и чистых side effects.

Отработайте React-вопросы в формате реального интервью

Тренируем объяснение Strict Mode, рендеринга и эффектов на практических кейсах с инженерной обратной связью.

Начать практику

FAQ

Почему React рендерит дважды именно в dev?

Потому что Strict Mode запускает дополнительные проверки корректности эффектов и очистки ресурсов. Это помогает обнаружить ошибки раньше релиза.

В production это поведение сохраняется?

Для стандартной production-сборки проверочные двойные вызовы Strict Mode не являются нормой. Если дубли остаются, ищите причину в логике приложения, повторных событиях или сетевой стратегии.

Нужно ли убирать Strict Mode, если тесты стали нестабильными?

Лучше стабилизировать тесты через корректную работу с эффектами, моками времени и очисткой окружения. Отключение режима обычно маскирует корневую проблему.

Почему useEffect с пустым массивом зависимостей все равно может сработать дважды?

В Strict Mode dev React проверяет, корректно ли работает cleanup, поэтому эффект может пройти дополнительный цикл setup -> cleanup -> setup.

Как быстро проверить, что проблема действительно в side effect?

Посмотрите, есть ли в эффекте cleanup, отмена async-операции и защита от устаревших данных. Затем сравните поведение dev и production build с профилированием.

Итоги

React «рендерит дважды» в dev не из-за случайного дефекта, а как часть инженерной проверки через Strict Mode. Этот механизм помогает находить ошибки, которые иначе проявятся поздно и дорого: утечки подписок, гонки запросов, неидемпотентные эффекты и грязный рендер.

Рабочая стратегия проста: не бороться с сигналом, а исправлять причину. Тогда dev-шум превращается в раннюю диагностику, а кодовая база становится устойчивее к сложным сценариям интерфейса и к вопросам на собеседовании.

Больше вопросов в Telegram

Ежедневные разборы и реальные кейсы с интервью

Подписаться

Автор

Lexicon Team

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