Почему React может рендерить дважды в dev режиме: Strict Mode без мифов
Разбираем, почему React в режиме разработки может вызывать рендер и эффекты дважды, какие баги это выявляет, как отличать норму от дефекта и как отвечать на собеседовании.
- Введение
- Что именно в React «вызывается дважды»
- Архитектура механизма: зачем React делает это в dev
- Контекст задачи
- Схема компонентов и поток управления
- Границы ответственности
- Узкие места и деградация
- Код-пример 1: почему дублируется аналитика
- Код-пример 2: двойной fetch в dev и корректная защита
- Сравнение подходов
- Production pitfalls: где команды чаще всего ошибаются
- Ошибка 1. «Починить» дубль отключением Strict Mode
- Ошибка 2. Не делать cleanup у подписок
- Ошибка 3. Мутировать внешнее состояние во время рендера
- Ошибка 4. Игнорировать race condition при fetch
- Разбор производительности: что считать нормой, а что проблемой
- Практики, которые работают в команде
- 1. Проектируйте эффекты так, чтобы их можно было безопасно запускать повторно (идемпотентность)
- 2. Следите, чтобы на этапе рендера не выполнялись побочные действия
- 3. Разделяйте эффекты по ответственности
- 4. Тестируйте быстрые mount/unmount сценарии
- 5. Фиксируйте инженерные правила в код-ревью
- Частые ошибки
- Как отвечать на интервью
- FAQ
- Почему React рендерит дважды именно в dev?
- В production это поведение сохраняется?
- Нужно ли убирать Strict Mode, если тесты стали нестабильными?
- Почему useEffect с пустым массивом зависимостей все равно может сработать дважды?
- Как быстро проверить, что проблема действительно в side effect?
- Итоги
Введение
Вопрос «почему React рендерит дважды» регулярно всплывает после включения Strict Mode и почти всегда вызывает одну и ту же реакцию: разработчик видит повторяющиеся записи в логах, считает это багом React и пытается устранить проблему, отключая режим.
Реальная картина сложнее. В dev режиме React действительно может повторно вызывать рендер и жизненный цикл эффекта, но это сделано как проверка устойчивости кода. Такой механизм помогает поймать дефекты раньше, чем они дойдут до пользователей.
Контекст Strict Mode удобнее читать параллельно с отдельным разбором режима и его проверок, потому что именно он определяет «двойное» поведение в разработке.
В статье разберем архитектуру процесса, типовые ловушки, разницу между dev и production, и практический шаблон ответа для собеседования.
Больше вопросов в Telegram
Ежедневные разборы и реальные кейсы с интервью
Что именно в React «вызывается дважды»
Фраза «React рендерит дважды» неточная. Важно разделять несколько уровней:
- Повторный вызов функции компонента в render phase.
- Повторный вызов setup-функции эффекта с промежуточным cleanup для проверки корректности (
setup -> cleanup -> setup). - Повторная инициализация небезопасных побочных эффектов, если код написан хрупко.
При этом двойной рендер в dev не означает двойной commit в DOM в том же смысле, как это было бы в production-инциденте. React в первую очередь проверяет, выдерживает ли компонент повторный запуск без утечек, гонок и неявных мутаций.
Если нужно освежить базовую модель рендеринга и где именно происходит commit, полезно посмотреть разбор когда компонент реально перерисовывается.
Архитектура механизма: зачем React делает это в dev
Контекст задачи
React-компоненты часто взаимодействуют с внешним миром: подписки, таймеры, fetch, аналитика и websocket-соединения. Ошибки в этих местах не всегда видны при «счастливом» сценарии, но всплывают при быстрых mount/unmount, повторной навигации и конкурентных обновлениях.
Схема компонентов и поток управления
Упрощенная схема для Strict Mode в dev:
- React вызывает render-функцию компонента.
- React коммитит изменения в дерево.
useEffectвыполняет setup.- React сразу запускает cleanup как проверку корректности.
- 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-симптомам.
Практический разбор:
- Сначала измеряйте production build в профайлере браузера.
- Затем отдельно анализируйте dev-шум от проверок Strict Mode.
- После фикса эффекта проверяйте, уменьшилось ли число лишних ререндеров в профиле.
Если компонент перегружен и часто обновляется, помогает архитектурное разделение состояния и мемоизация на границах. Детальный разбор, когда это оправдано, есть в материале про 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.
Как отвечать на интервью
Хороший короткий ответ:
- В dev React Strict Mode может повторять рендер и цикл эффекта, чтобы выявить неидемпотентные side effects.
- Это диагностическое поведение, не равное production-семантике.
- Если видите дубль запроса, нужно исправлять эффект: cleanup, abort, защита от гонок.
- Отключение 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
Читайте также
frontend
Как устроен createRoot в React v18+: root API, concurrency и миграция с ReactDOM.render
Подробно разбираем createRoot в React v18+: что меняется в инициализации приложения, как работает root API, где типично ошибаются и как отвечать на интервью.
frontend
Почему React использует ключи (key): как работает идентичность элементов
Разбираем, зачем React использует key, как ключи помогают reconciliation, почему без них теряется state и как объяснить это на собеседовании.
frontend
React Strict Mode: зачем он нужен
Подробно разбираем React Strict Mode: какие проверки он включает, почему в dev все «вызывается дважды», какие баги ловит и как безопасно внедрять в production-командах.