Перейти к основному контенту
Tech Path Finder
КурсыИнтервьюКод-ревьюБлог
Tech Path Finder

Персонализированный путеводитель в IT. Квизы, мок-интервью, код ревью и аналитика прогресса.

@potapov_me

Платформа

  • Курсы
  • Прогресс
  • Мок-интервью
  • Код ревью
  • Живое ревью с ИИ
  • Тренажёр переговоров
  • Закладки

Контент

  • Блог
  • Главная
  • Обратная связь

Компания

  • О проекте
  • Тарифы
  • Условия использования
  • Конфиденциальность
  • Согласие на обработку данных
  • Cookie
  • Реквизиты

Аккаунт

  • Войти
  • Зарегистрироваться
  • Профиль

© 2026 Tech Path Finder. Все права защищены.

·ИП Потапов К.С.·Политика конфиденциальности·
Сделано с ❤️ в России
  1. Модель рендеринга React
rendering_model

Модель рендеринга React

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

Открыть лабораториюv0.2.0Запускается локально из публичного репозитория

Модель рендеринга React

React проще отлаживать, если помнить: компонент вычисляет следующий вид интерфейса, а не редактирует экран по шагам.

#Результат урока

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

Понадобятся чистые функции-компоненты, свойства, состояние и понимание ссылок на объекты. В конце вы соберёте интерактивное дерево, где можно увидеть этапы отрисовки и фиксации, а также намеренно сохранить или сбросить состояние элемента.

#1. От запроса обновления до изменения DOM

Обновление ставит работу в очередь. Сначала React вызывает компоненты и вычисляет следующее дерево (этап отрисовки, render). Затем он фиксирует результат: вносит необходимые изменения в DOM (этап фиксации, commit). Первую часть React может повторить или отбросить, поэтому во время вызова компонента нельзя измерять DOM и запускать внешние действия.

Порядок этапов виден по журналу — сообщения появятся именно в такой последовательности:

// src/features/demo/PhaseLog.tsx function PhaseLog({ label }: { label: string }) { console.log('1. отрисовка: компонент вызван'); // может повториться или быть отброшен useEffect(() => { console.log('3. эффект: DOM уже обновлён'); }); return <div ref={() => console.log('2. фиксация: ref получил узел')}>{label}</div>; }

Отсюда следует практическое правило: element.getBoundingClientRect() во время вызова компонента вернёт размеры прошлого кадра, а отправка аналитики оттуда же может выполниться дважды или ни разу — React вправе отбросить незавершённую отрисовку.

Проверьте себя. Запишите в журнал вызов компонента, обратный вызов ref и useEffect. Сравните порядок сообщений.

Частая ошибка. Считать, что каждый вызов компонента обязательно изменяет DOM.

#2. Состояние как снимок

Вызов функции setCount не меняет переменную count в уже работающем обработчике. Он просит React выполнить компонент ещё раз с новым снимком (snapshot) состояния. Поэтому старый обработчик продолжает видеть прежние свойства и состояние. Если новое значение зависит от предыдущего, передавайте функцию обновления.

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

function addThreeWrong() { // count в этом вызове — это снимок, он равен 0 все три раза setCount(count + 1); // ставит в очередь «стать 1» setCount(count + 1); // ставит в очередь «стать 1» setCount(count + 1); // итог: 1 } function addThree() { setCount(value => value + 1); setCount(value => value + 1); setCount(value => value + 1); // итог: 3 }

Каждая функция получает результат предыдущего обновления, поэтому итог увеличивается на три. Тот же механизм объясняет «устаревшее» значение в асинхронном коде:

function handleSave() { setStatus('saving'); void save().then(() => { // здесь status по-прежнему прежний: это снимок того вызова компонента console.log(status); // не 'saving' }); }

Проверьте себя. Сравните три вызова setCount(count + 1) с тремя вызовами, принимающими функцию.

Частая ошибка. Ожидать, что count изменится между вызовами setCount внутри одного обработчика.

#3. Группировка обновлений

React собирает обновления, сделанные в одном обработчике, и применяет их вместе (batching). Пользователь не видит промежуточные состояния, а DOM меняется реже. Это не означает, что React сам объединит объекты состояния. flushSync нужен лишь для редких интеграций, которым требуется уже обновлённый DOM прямо сейчас.

Три обновления в одном обработчике дают одну фиксацию:

function handleSubmit() { setStatus('saving'); // setError(null); // одна фиксация DOM на все три setAttempts(n => n + 1); }

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

function handleAddRow() { flushSync(() => setRows(list => [...list, createRow()])); // DOM обновлён здесь listRef.current?.lastElementChild?.scrollIntoView(); // измеряем уже новый узел }

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

Проверьте себя. Подсчитайте фиксации DOM после нескольких обновлений в одном событии и проверьте итоговое состояние.

Частая ошибка. Вызывать flushSync после каждого обновления ради привычной пошаговой последовательности.

#4. Состояние связано с позицией, типом и key

Состояние хранится у React и связано с местом компонента в дереве. Одинаковые тип и key в той же позиции сохраняют состояние. Если тип или key изменился, React создаёт новое поддерево. По этой же причине не объявляйте один компонент внутри другого: при каждом вызове получится новый тип.

key — это способ явно сказать «это другая сущность, состояние не переносить»:

// без key состояние формы перенесётся на другую заявку <TicketForm ticket={selected} /> // с key при смене заявки форма пересоздаётся с чистым состоянием <TicketForm key={selected.id} ticket={selected} />

Объявление компонента внутри компонента ломает это правило полностью:

// ❌ Row — новый тип функции на каждой отрисовке Table: состояние сбрасывается всегда function Table({ rows }: { rows: Row[] }) { function RowView({ row }: { row: Row }) { const [open, setOpen] = useState(false); // будет теряться при любом обновлении Table return <tr onClick={() => setOpen(!open)}>{open ? row.details : row.title}</tr>; } return <tbody>{rows.map(row => <RowView key={row.id} row={row} />)}</tbody>; }

React сравнивает типы по идентичности функции. Новая функция на каждой отрисовке означает новый тип, поэтому старое поддерево размонтируется вместе со всем состоянием — включая фокус, введённый текст и позицию прокрутки.

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

Частая ошибка. Объявлять компонент строки внутри родительского компонента.

#5. Как React сопоставляет деревья

React сопоставляет соседние элементы предыдущего и нового дерева по типу и key, а затем обновляет изменившиеся свойства DOM-узлов. Детали устройства Fiber не являются контрактом приложения: нельзя опираться на число внутренних узлов или порядок их обхода.

Ключ по индексу совпадает с доменным до первой перестановки — и расходится с ней:

// ❌ после удаления первой строки ключи сдвигаются: состояние строк «съезжает» {tickets.map((ticket, index) => <TicketRow key={index} ticket={ticket} />)} // ✅ ключ привязан к сущности, а не к позиции {tickets.map(ticket => <TicketRow key={ticket.id} ticket={ticket} />)}

Случайный ключ — обратная крайность: он гарантированно размонтирует и создаёт узел заново на каждой отрисовке.

// ❌ полный сброс поддерева каждый раз: потеря фокуса, прокрутки, анимаций <TicketRow key={Math.random()} ticket={ticket} />

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

Проверьте себя. Поменяйте тип корневого элемента поддерева и проследите за сбросом его состояния.

Частая ошибка. Использовать случайный key как универсальный способ «обновить» компонент.

#6. StrictMode как проверка устойчивости

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

Двойной вызов проявляет именно те дефекты, которые в рабочей сборке дают редкие невоспроизводимые баги:

// ❌ StrictMode покажет два обработчика: очистка не снимает подписку useEffect(() => { window.addEventListener('resize', () => setWidth(window.innerWidth)); // возврата нет — обработчик остаётся навсегда }, []); // ✅ симметричная очистка: сколько добавили, столько и снимаем useEffect(() => { const handleResize = () => setWidth(window.innerWidth); window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, []);

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

// ❌ утечка осталась: при повторном монтировании подписка уже не создастся вовсе let didRun = false; useEffect(() => { if (didRun) return; didRun = true; subscribe(); }, []);

После такого «исправления» компонент перестанет работать при повторном открытии страницы — а именно это делает навигация в одностраничном приложении.

Проверьте себя. Добавьте подписку с очисткой и убедитесь, что после повторной проверки остаётся один обработчик события.

Частая ошибка. Ставить флаг модуля didRun, скрывая утечку вместо её исправления.

#7. Конкурентный рендеринг и срочность

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

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

function handleChange(event: React.ChangeEvent<HTMLInputElement>) { setQuery(event.target.value); // срочно: символ должен появиться сразу startTransition(() => setFilter(event.target.value)); // несрочно: список подождёт }

Если поменять роли и обновлять значение поля внутри перехода, ввод начнёт «залипать»: React вправе отложить несрочное обновление, и символ появится с задержкой. Управляемое поле всегда обновляется срочно.

Проверьте себя. Оставьте обновление поля ввода срочным, а пересчёт большого списка оберните в startTransition.

Частая ошибка. Поместить обновление управляемого поля в переход и получить задержку при наборе.

#8. Трассировка без изменения поведения

Диагностика не должна сама вызывать новые отрисовки или менять ссылки на свойства. Счётчик вызовов компонента полезен только вместе с данными о фиксациях и причинах обновления. React DevTools Profiler показывает длительность фиксаций, а console.log внутри компонента — все вызовы, включая отброшенные.

Счётчик в состоянии — это диагностика, которая сама создаёт то, что измеряет:

// ❌ каждое обновление счётчика вызывает новую отрисовку: бесконечный цикл const [renderCount, setRenderCount] = useState(0); setRenderCount(renderCount + 1); // ✅ ref не вызывает отрисовку и не участвует в сравнении свойств const renderCount = useRef(0); renderCount.current += 1;

Даже корректный счётчик отвечает только на вопрос «сколько раз вызван», но не «сколько это стоило». Сто вызовов по 0.05 мс дешевле одного на 30 мс, а часть вызовов может быть отброшена и вообще не дойти до DOM. Поэтому число сообщений сопоставляют с длительностью фиксаций в Profiler.

Проверьте себя. Сопоставьте сообщения компонента с фиксациями Profiler для перехода, прерванного новым вводом.

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

#Лаборатория

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

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

#Источники для сверки

  • React: Render and Commit
  • React: State as a Snapshot
  • React: Preserving and Resetting State
  • React StrictMode

Устранение неисправностей

Далее: События и локальное состояние