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

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

@potapov_me

Платформа

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

Контент

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

Компания

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

Аккаунт

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

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

·ИП Потапов К.С.·Политика конфиденциальности·
Сделано с ❤️ в России
  1. Внутреннее устройство и миграция
internals_migration

Внутреннее устройство и миграция

Модель Fiber без зависимости от деталей реализации, планировщик, классовые компоненты, переход с React 18 на 19 и проверка кода старшего уровня.

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

Внутреннее устройство и миграция

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

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

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

Понадобится весь маршрут: отрисовка, конкурентность, серверные границы, производительность и эксплуатация. Итог урока — проект миграции React 18→19 и React Compiler с диагностическими проверками, матрицей совместимости, постепенным включением и откатом.

#1. Публичный контракт и внутреннее устройство

Публично можно опираться на чистоту отрисовки, связь состояния с компонентом, жизненный цикл эффекта и документированные API. Узлы Fiber, внутренние флаги, номера полос (lanes) и точный порядок планирования — детали реализации. Диагностическая проверка и фиксация версии допустимы, но корректность продукта от них не зависит.

Разница видна по имени того, к чему вы обращаетесь:

// ❌ внутреннее устройство: имя ключа и структура меняются между версиями const fiberKey = Object.keys(node).find((key) => key.startsWith('__reactFiber$'))!; const props = (node as any)[fiberKey].memoizedProps; // сломается на минорном обновлении // ✅ публичный контракт: документированный API и данные из атрибутов const value = (node as HTMLInputElement).value; const ticketId = node.dataset.ticketId;

Первый вариант встречается в самописных инструментах отладки и в тестах, «которым нужно посмотреть состояние». Он превращает обновление React в исследовательскую задачу: приложение собирается, тесты проходят, а инструмент молча возвращает undefined.

Проверьте себя. Пометьте утверждения как гарантию, наблюдение или предположение.

Частая ошибка. Читать внутренний Fiber из DOM-узла.

#2. Планировщик и модель приоритетов

Обновления имеют разную срочность; React планирует вычисление дерева и может прервать переход срочным вводом. Фиксация остаётся согласованной, а эффекты относятся только к зафиксированному дереву. Не обещайте точные миллисекунды или порядок между независимыми корнями. Долгая задача в основном потоке браузера всё равно блокирует React.

Конкурентность — это не второй поток, а возможность прервать работу между единицами:

// ❌ 200 мс синхронной работы: прервать нечем, React ждёт вместе с браузером function Results({ items }: { items: Item[] }) { const sorted = heavySortInPlace(items); // блокирует основной поток целиком return <List items={sorted} />; }

Прерываемость даёт только разбиение работы и отметка её несрочной:

// ✅ несрочное обновление можно прервать вводом пользователя const [isPending, startTransition] = useTransition(); function handleFilter(next: Filter) { startTransition(() => setFilter(next)); // React вправе отложить и переработать }

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

Проверьте себя. Снимите трассировку срочного ввода и перехода.

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

#3. Перенос жизненного цикла классов без механики

componentDidMount, componentDidUpdate и componentWillUnmount часто объединяют разные виды синхронизации. При миграции разделите эффекты по внешним ресурсам, производные значения верните в отрисовку, логику событий — в обработчики. Границы ошибок пока обычно остаются классовым компонентом или частью фреймворка. Не переносите весь жизненный цикл в один огромный эффект.

Типичный componentDidUpdate содержит три разные обязанности в одном месте:

// ❌ было: подписка, производное значение и аналитика в одном методе componentDidUpdate(prevProps: Props) { if (prevProps.roomId !== this.props.roomId) { this.connection?.close(); this.connection = connect(this.props.roomId); } this.setState({ fullName: `${this.props.firstName} ${this.props.lastName}` }); analytics.track('room_updated', { roomId: this.props.roomId }); }

После разделения каждая обязанность попадает туда, где ей место:

// ✅ стало: внешний ресурс — эффект, производное — отрисовка, событие — обработчик useEffect(() => { const connection = connect(roomId); return () => connection.close(); // симметричная очистка }, [roomId]); const fullName = `${firstName} ${lastName}`; // вычисляется во время отрисовки function handleRoomChange(next: string) { analytics.track('room_updated', { roomId: next }); // вызвано событием, не отрисовкой onRoomChange(next); }

Механический перенос всего метода в один useEffect без зависимостей сохраняет исходную путаницу и добавляет новую: производное значение начинает отставать на одну фиксацию, а подписка пересоздаётся при любом изменении свойств.

Проверьте себя. Разделите устаревший componentDidUpdate по обязанностям.

Частая ошибка. Эффект без массива зависимостей как копия всего жизненного цикла.

#4. Проверка несовместимых изменений React 19

Перед обновлением изучите официальное руководство: требование нового преобразования JSX, удалённые API, очистку ref, удаление ReactDOM.render и изменения типов. Сначала устраните предупреждения последней версии 18.3, затем применяйте React 19, кодовые преобразования (codemods) и тесты. Не смешивайте крупное обновление фреймворка, React и архитектуры в одном развёртывании.

Порядок шагов важнее скорости, и его стоит зафиксировать явно:

# 1. предупреждения текущей ветки — на 18.3 они те же, что станут ошибками в 19 npm i react@18.3 react-dom@18.3 && npm test # 2. автоматические преобразования кода npx codemod@latest react/19/migration-recipe # 3. только теперь обновление мажорной версии npm i react@19.2 react-dom@19.2 && npm run typecheck && npm test

Матрица совместимости важна не меньше: библиотеки с собственными peerDependencies на React часто отстают от мажорного релиза.

ЗависимостьТекущаяПоддерживает React 19Действие
react-router8.3даобновить вместе с React
@testing-library/react15.xс 16.0обновить до 16.x
старый UI-набор3.xнетзаменить или изолировать адаптером

Обновление диапазонов в package.json без файла блокировки и прогона тестов даёт худший вариант: сборка проходит, а несовместимость обнаруживается у пользователя.

Проверьте себя. Составьте матрицу совместимости зависимостей.

Частая ошибка. Обновить диапазоны в package.json без файла блокировки и тестов.

#5. Замена устаревших API

Для строковых ref, findDOMNode, ReactDOM.render и значений propTypes по умолчанию документированы пути миграции. Сохраняйте поведение через createRoot или hydrateRoot, свойство или обратный вызов ref, явное владение DOM и параметры по умолчанию. findDOMNode особенно нарушает инкапсуляцию и допущения конкурентной отрисовки.

findDOMNode находит узел в обход контракта компонента — и потому ломается, как только структура разметки меняется:

// ❌ было: обёртка достаёт чужой DOM-узел class FocusWrapper extends React.Component<Props> { componentDidMount() { const node = ReactDOM.findDOMNode(this) as HTMLElement; node.querySelector('input')?.focus(); // предполагает вёрстку потомка } }

Явное владение узлом делает связь видимой в типах: компонент получает ref тем же способом, что и любое другое свойство:

// ✅ стало: узел передан явно, обёртка не знает про вёрстку function FocusOnMount({ children, targetRef }: { children: React.ReactNode; targetRef: React.RefObject<HTMLInputElement | null> }) { useEffect(() => { targetRef.current?.focus(); }, [targetRef]); return <>{children}</>; } // в React 19 ref передаётся обычным свойством, forwardRef не нужен function TitleInput({ ref }: { ref?: React.Ref<HTMLInputElement> }) { return <input ref={ref} />; }

Замена findDOMNode на глобальный querySelector не решает проблему, а расширяет её: теперь компонент зависит не от разметки потомка, а от разметки всей страницы, и найдёт чужое поле при первом же переиспользовании.

Проверьте себя. Удалите обёртку с findDOMNode.

Частая ошибка. Глобальный querySelector как замена.

#6. Постепенное включение Compiler в старом коде

Сначала обновите eslint-plugin-react-hooks, исправьте нарушения и запустите проверку готовности. Включайте компиляцию по каталогам или компонентам, фиксируйте версию и сравнивайте тесты и производительность. Существующую ручную мемоизацию оставляйте до измеренного удаления. panicThreshold и режим компиляции управляют внедрением, а не скрывают ошибки.

Ограничение области — это настройка сборки, а не правка кода:

// vite.config.ts — компилируем только уже проверенный каталог react({ babel: { plugins: [['babel-plugin-react-compiler', { target: '19', sources: (filename: string) => filename.includes('/src/features/tickets/'), }]], }, });

Дальше маршрут включают части пользователей и сравнивают метрики с контрольной группой:

// src/app/routes.ts const TicketsRoute = flags.compiledTickets ? CompiledTickets : LegacyTickets; // 10% выкатки

Удаление ручной мемоизации — отдельный шаг после измерения. Один коммит «скомпилировали всё и убрали memo» соединяет два независимых источника регрессии: если производительность упадёт, вы не узнаете, кто виноват. panicThreshold при этом не «исправляет» проблемные компоненты — он лишь решает, падать сборке или пропускать такой компонент без компиляции.

Проверьте себя. Включите скомпилированный маршрут для 10% пользователей.

Частая ошибка. Скомпилировать всё и удалить memo одним изменением.

#7. Вертикальный срез вместо переписывания по слоям

Мигрируйте один маршрут или сценарий вместе с тестами, границами и наблюдаемостью, оставляя адаптеры к старому коду. Вертикальный срез даёт пользовательскую ценность и реальный сигнал; слой «перепишем все утилиты» долго не поставляется. После переноса потребителей временный адаптер удаляют.

Адаптер — временный мост между новым и старым владельцем данных, и он должен быть помечен как временный:

// src/features/tickets/legacy-bridge.ts // TODO(migration): удалить после переноса /reports и /admin на useTickets (срок: 2026-10-01) export function subscribeLegacyStore(onChange: (tickets: Ticket[]) => void): () => void { const unsubscribe = legacyStore.subscribe(() => onChange(legacyStore.getTickets())); return unsubscribe; }

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

Проверьте себя. Выберите срез с высокой пользой и ограниченной областью риска.

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

#8. Архитектурная проверка: доказательство и остаточный риск

Проект решения содержит цель и ограничения, перечень кода, матрицу совместимости, альтернативы, этапы, метрики, влияние на безопасность и производительность, откат и открытые риски. «Современнее» не аргумент. После пробного включения сравните ошибки, Web Vitals, успешность сценариев и обращения в поддержку. Иногда правильное решение — пока не мигрировать.

Проект решения умещается на страницу, и его проверяют по наличию каждого пункта:

Цель: сократить INP p75 на /tickets с 420 до ≤200 мс, снять ручную мемоизацию Ограничения: без остановки поставки, откат в течение 10 минут, старый UI-набор 3.x не поддерживает React 19 Этапы: 18.3 + предупреждения → codemods → React 19.2 → Compiler на /tickets (10/50/100%) Метрики: INP p75, доля ошибок границ, успешность сохранения заявки, обращения в поддержку Альтернативы: точечная виртуализация без миграции (дешевле, не снимает мемоизацию); остаться на 18.3 (нулевой риск, не решает задачу) Откат: флаг compiledTickets → выключение без развёртывания; артефакт N-1 хранится Остаточный риск: UI-набор 3.x изолирован адаптером, но блокирует обновление до 20.x

Раздел с альтернативами и остаточным риском — то, что отличает проект от намерения. Успешная сборка не является критерием: она не говорит ни о задержке ввода, ни о доле ошибок, ни о том, стало ли пользователю лучше. И вывод «пока не мигрируем, потому что блокирующая зависимость не готова» — это полноценный результат проверки.

Проверьте себя. Защитите проект миграции перед критически настроенными коллегами.

Частая ошибка. Считать успешную сборку единственным критерием успеха.

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

Проверьте старую панель: классы, findDOMNode, строковые ref, побочные эффекты во время отрисовки, ручную мемоизацию, загрузку данных эффектами и старый корень. Перенесите один вертикальный срез на React 19, действия, ref как свойство и React Compiler, сохранив тесты поведения и план пробного включения.

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

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

  • React 19 Upgrade Guide
  • React Legacy APIs
  • React Compiler incremental adoption

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