Реконсиляция: эвристики диффа и ловушка ключей
React сводит O(n³) дифф деревьев к O(n) двумя эвристиками — одинаковый тип сохраняет состояние, разный тип уничтожает его. Ключи идентифицируют элементы списка между рендерами: индексные ключи молча портят неуправляемое состояние при перестановке.
Чекбокс, отмеченный в строке 3, остаётся отмеченным после удаления строки 1 — теперь на том месте, что было строкой 4. Никакого предупреждения React. Никакой ошибки в консоли. Одно изменение — key={index} на key={item.id} — и баг исчезает. Ключи — не подсказки. Это механизм, которым React сопоставляет файберы с элементами между рендерами, и если ошибиться — состояние молча портится.
Реконсиляция: правила диффа. Реконсиляция сравнивает новое дерево элементов с текущим fiber-деревом, чтобы решить, что сохранить, обновить или заменить. Полный дифф дерева — O(n³) в общем случае; React делает его O(n) двумя эвристиками.
Эвристика 1 — одинаковый тип, тот же fiber. Если <div> остаётся <div> или <ComponentA> остаётся <ComponentA>, React обновляет существующий fiber на месте — патчит props, запускает реконсиляцию для потомков. Fiber сохраняет своё состояние.
Эвристика 2 — разный тип, полная замена. Если <div> становится <span> или <ComponentA> становится <ComponentB>, React сносит всё поддерево (размонтирует его, уничтожает состояние) и строит новое с нуля. Он не пытается делать дифф через границу типов. Именно поэтому условный рендеринг двух разных компонентов в одном слоте сбрасывает всё состояние — даже если новый компонент выглядит похоже.
Ключи идентифицируют дочерние элементы списка между рендерами. В списке React сопоставляет старые и новые дочерние элементы по их key, а не по позиции. Стабильный ключ говорит React: «это тот же логический элемент, даже если он переместился». В этом и есть весь смысл ключей. React строит карту из key в старый fiber, затем обходит новые дочерние элементы, сопоставляя каждый со старым fiber по ключу. Сопоставленный fiber несёт вперёд всё статeful: значения useState, содержимое useRef, неуправляемое состояние DOM (введённый текст input, отмеченность checkbox, позицию скролла, фокус) и сам DOM-узел.
Почему key={index} — ловушка. С key={index} ключ — это позиция, поэтому при перестановке списка или вставке элемента в начало ключ каждого элемента смещается. React тогда считает, что элемент-по-индексу-0 — «тот же элемент», хотя данные переместились — он сохраняет состояние и DOM старого fiber, но передаёт ему props нового элемента. Видимый баг: чекбокс, отмеченный в строке 3, остаётся отмеченным после удаления строки 1, теперь прикреплённый к тому, что было строкой 4.
Управляемые props обновляются корректно (они передаются заново при каждом рендере); неуправляемое состояние — нет (оно живёт на fiber). key={index} безопасен только для списка, который статичен и никогда не переупорядочивается; для любого динамического списка он молча портит состояние.
▸Граничные случаи
Math.random() в качестве ключа хуже, чем key={index}. Это присваивает новый ключ при каждом рендере, поэтому React всегда видит «другой» элемент на каждой позиции — он размонтирует и перемонтирует каждый элемент списка при каждом обновлении. Вы получаете правильное состояние (потому что fiber всегда новый), но платите за размонтирование + монтирование каждого элемента при каждом нажатии клавиши. key={index} неправильный для динамических списков; key={Math.random()} неправильный для всех списков.
Список элементов рендерится с `key={index}`. Вы удаляете первый элемент. Чекбокс, который был отмечен на старом втором элементе, теперь отмечен на неправильной строке. Почему?
Вы рендерите `condition ? <ComponentA /> : <ComponentB />` в одном слоте. Когда условие меняется, что происходит с состоянием ComponentA?
Почему управляемое состояние (value, checked, переданные как props) выглядит правильно с `key={index}`, а неуправляемое (введённый текст, позиция скролла) — нет?
- 01Объясните на уровне fiber, почему key={index} портит состояние при перестановке списка.
- 02Что делает React, когда тип компонента меняется в одном слоте?
- 03Когда key={index} действительно безопасен?
Реконсиляция сводит O(n³) дифф деревьев к O(n) двумя эвристиками: одинаковый тип элемента означает обновление fiber на месте (состояние сохраняется); разный тип означает снос поддерева и перестройку с нуля (состояние уничтожается). Для списков React использует key для сопоставления новых дочерних элементов со старыми файберами по идентичности, а не позиции. Fiber несёт всё stateful — значения useState, рефы, неуправляемое состояние DOM — поэтому сопоставление неправильного fiber с неправильным элементом молча портит это состояние. key={index} — ловушка: индексные ключи позиционные, поэтому любая перестановка, вставка или удаление смещает, какой fiber достанется элементу. Используйте стабильный ключ идентичности (id из базы данных, slug) всегда, когда список динамический.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
встречается в169
- Почему GraphQL получает N+1junior
- Механика DataLoader: батчинг на границе тикаmiddle
- Контракты batch-функции: порядок, формы, ошибкиmiddle
- Federation и lookahead: батчинг за пределами DataLoadermiddle
- Защита сложности запросов: depth, cost, persisted queriesmiddle
- Senior GraphQL API: scheduling-контракт, изоляция арендаторов, наблюдаемостьsenior
- Зачем идемпотентность: безопасные retryjunior
- Серверный state machine: четыре состояния idempotency keymiddle
- Outbox и inbox: effectively-once через dual-write границуmiddle
- Конкурентность и архитектура кеша для идемпотентности на масштабеsenior
- Наблюдаемость, production-инциденты и дизайн для глобального масштабаsenior
- Что такое cache stampede и почему он делает всё хужеjunior
- Лок и single-flight: ограничение параллельных rebuildmiddle
- XFetch: вероятностное раннее истечение без координацииmiddle
- Stale-while-revalidate и CDN request coalescingmiddle
- Детектирование stampede и дизайн TTL для продакшенаmiddle
- Метастабильный сбой, fencing-токены и production-постмортемыsenior
- Что такое отношение: таблицы, строки, ключи и ограниченияjunior
- Ограничения, ключи и типы данных Postgresmiddle
- Нормальные формы, денормализация и почему схемы «прилипают»middle
- JSONB, массивы и когда side table побеждаетmiddle
- Heap-хранилище, TOAST и выравнивание колонокsenior
- Целостность схемы: deferral, версионирование и сбои в продакшнеsenior
- Реляционная модель vs документные, wide-column, граф и key-valuesenior
- Index-only scan, Visibility Map и INCLUDEsenior
- Типичные сбои в продакшне и аудит индексовsenior
- pg_statistic, ANALYZE и производственная наблюдаемостьmiddle
- Производственные режимы отказа и стабильность плановsenior
- MVCC: как Postgres раздаёт согласованные снимкиjunior
- Заголовок tuple и механика снимковmiddle
- HOT-обновления и уровни изоляцииmiddle
- VACUUM, bloat и autovacuummiddle
- CLOG, XID wraparound и MultiXactsenior
- SSI и production-тюнинг autovacuumsenior
- Реальные провалы MVCC, deployment-паттерны и распределённые снимкиsenior
- Connection pool: зачем амортизировать стоимость backend Postgresjunior
- Режимы PgBouncer: session, transaction и statementmiddle
- Размер пула: формула (ядра × 2) + шпинделей и двухуровневый стекmiddle
- Исчерпание пула и idle-in-transaction: сценарий отказа в 3 ночиmiddle
- Миграция на transaction mode: план развёртывания и prepared statements в PgBouncer 1.21middle
- Процессная модель Postgres и почему увеличение max_connections снижает производительностьsenior
- Ландшафт пулеров 2026, serverless connection storms и полная таксономия отказовsenior
- Что такое миграция схемы и почему она заменяет ad-hoc DDLjunior
- ADD COLUMN: мгновенно в PG 11+ против перезаписи в старом Postgresjunior
- Режим отказа очереди блокировок: почему мгновенный DDL может заморозить базуmiddle
- Безопасные DDL-паттерны: NOT VALID, CONCURRENTLY и исправления небезопасных операцийmiddle
- Expand-contract: нулевой простой для ломающих изменений схемыmiddle
- Advisory-блокировки, инструменты миграций и координация деплояsenior
- Таксономия сбоев миграций и дисциплина продакшнаsenior
- Зачем нужно шардирование: потолок одного Postgresjunior
- Выбор ключа шарда: стратегии hash, range, list и directorymiddle
- Партиционирование против шардирования: одно слово, два разных понятияmiddle
- Ко-локация и Citus: инвариант, делающий шардирование пригодным к использованиюmiddle
- Режим отказа hot shard: обнаружение, изоляция и долгосрочная политикаmiddle
- Schema-based шардирование и альтернативы мультиарендностиsenior
- Онлайн-решардинг, 2PC и операционная стоимость шардированияsenior
- Семь актов: от CREATE TABLE до Citusjunior
- Акты 1–3 в глубину: схема, индексы и статистика планировщикаmiddle
- Акты 4–6 в глубину: MVCC bloat, connection pooling и безопасные миграцииmiddle
- Акт 7 в глубину: шардинг, co-location и семиуровневый каскад трейдоффовmiddle
- Наблюдаемость, антипаттерны и производственный триажsenior
- Роли Raft, term и почему majority-кворум предотвращает split brainjunior
- Как Raft реплицирует log entry и решает, что его безопасно коммититьmiddle
- Выборы лидера в Raft: таймауты, правила голосования и четыре свойства безопасностиmiddle
- Raft в реальном мире: partition, медленный диск и клиентская маршрутизацияmiddle
- Расширения Raft: pre-vote, learner, snapshot и линеаризуемые чтенияsenior
- Raft в production: membership change, Multi-Raft и observabilitysenior
- Где происходит data fetching — и почему это решает LCPjunior
- Fetch waterfall''''ы — диагностика и лечение через Promise.allmiddle
- React Server Components и Suspense streamingmiddle
- Клиентский кэш: TanStack Query, SWR и stale-while-revalidatemiddle
- LCP, prefetch и race conditions в интерактивном fetchingmiddle
- Senior internals: RSC payload, слои кэша и production паденияsenior
- Что такое движок JavaScriptzero
- Ignition и байт-кодmiddle
- Внутри цикла интерпретатораmiddle
- Как V8 представляет значениеmiddle
- Что такое Map на самом деле: вскрываем hidden classmiddle
- Деревья переходов, deprecation и миграцияmiddle
- Быстрые свойства, медленные свойства и slack trackingmiddle
- Inline cache в глубину: feedback-слоты и handlersmiddle
- Monomorphic, polymorphic, megamorphic — и stub cachemiddle
- Четыре яруса и как работает разогревmiddle
- Обратная связь о типах: топливо для оптимизатораsenior
- TurboFan: оптимизатор на море узловsenior
- Спекуляция и guardssenior
- Деоптимизация: падение с обрываsenior
- On-Stack Replacement: подмена фрейма посреди циклаsenior
- Области видимости, контексты и scope chainmiddle
- Что на самом деле удерживает замыканиеmiddle
- Когда V8 аллоцирует Context (и чего это стоит)middle
- Замыкания, feedback-векторы и полиморфизм точки вызоваmiddle
- Зачем нужен GC и достижимостьmiddle
- Утечки в языке со сборкой мусораsenior
- Движок против циклаmiddle
- Микрозадачи против макрозадач: порядокmiddle
- Внутри Promisemiddle
- async/await, десахаризированныйmiddle
- Капстоун: оптимизируем горячий путьsenior
- Трёхстороннее рукопожатие TCPjunior
- Номера последовательности и состояние соединенияmiddle
- DNS: что делает и зачем существуетjunior
- Обход резолвера: перенаправления, типы записей и gluemiddle
- TTL, кеширование и распространение DNSmiddle
- Рукопожатие за 1 RTT: key share и ECDHEmiddle
- Возобновление сессии и 0-RTTmiddle
- WebSocket: HTTP-апгрейд до постоянного соединенияjunior
- Формат WebSocket-фрейма: opcodes, маскирование, фрагментацияmiddle
- Backpressure в WebSocket: когда клиенты не успеваютmiddle
- Реконнект: jittered backoff, thundering herd, восстановление сообщенийsenior
- WebSocket в масштабе: HTTP/2 мультиплексирование, permessage-deflate, C10Msenior
- WebSocket в production: прокси, безопасность и распределённая архитектураsenior
- Что делают обратные проксиjunior
- Health checks, connection draining и slow startmiddle
- Session affinity, consistent hashing и правильное решениеmiddle
- Retry-бури, circuit breakers и load sheddingsenior
- Устойчивая архитектура LB: anycast, zone-aware маршрутизация и observabilitysenior
- Почему QUIC, а не TCP+TLSjunior
- Connection ID и миграция сетиmiddle
- Возобновление 0-RTT и шифрование пакетовsenior
- DDoS: что это и почему работаетjunior
- Атаки усиления и истощение состоянияmiddle
- Ограничение скорости: алгоритмы и архитектураmiddle
- WAF, межсетевые экраны, mTLS и HSTSmiddle
- Отравление DNS-кэша и BGP-перехватsenior
- Эшелонированная защита и экономика атакsenior
- DNS, TCP, TLS по очереди: куда уходят миллисекундыmiddle
- Перехват прокси и шлюзы безопасности: rate limiter, WAF, mTLSmiddle
- Альтернативные пути: QUIC 0-RTT, WebSocket upgrade, миграция соединенияmiddle
- Наблюдаемость: распределённые трейсы, USE/RED и семплированиеsenior
- Устойчивость: каскадные повторы, circuit breakers и error budgetsenior
- Что такое три сигнала: метрики, логи, трейсыjunior
- Зачем нужны структурные логи: дневник против таблицыjunior
- Схема продакшн-лога: поля, которые несёт каждая строкаmiddle
- PII-редакция и log injectionsenior
- OTel Logs Data Model и audit-логи как подсистемаsenior
- SLI, SLO и error budget: надёжность в числахjunior
- Error budget policy, latency SLO и составные journeysmiddle
- Продакшн-отказы SLO, самонаблюдаемость, безопасность и общая картинаsenior
- Петля инцидента: от пейджера до постмортема до предотвращенияmiddle
- Cache lines и false sharing: когда параллелизм замедляет кодmiddle
- SIMD и data layout: AoS vs SoA и разница в 4–8xmiddle
- Cache-oblivious алгоритмы, PGO и production failuressenior
- GC в production: наблюдаемость, безопасность, edge cases и управление флотомsenior
- Batching: амортизируй фиксированную цену каждой операцииjunior
- Окно батчинга: размер и время ожиданияmiddle
- Batching в Kafka и Postgresmiddle
- io_uring и наблюдаемость пакетированияmiddle
- От Nagle до io_uring: эволюция пакетированияmiddle
- Backpressure, изоляция сбоев и безопасность батчей в продакшенеsenior
- CI enforcement и RUM: делаем бюджеты рабочимиmiddle
- V8 JIT-пайплайн, HTTP-приоритеты и безопасность bundlesenior
- Цикл performance: дисциплина, а не проектjunior
- Классификация и исправление: сопоставление family bottleneck с методамиmiddle
- Observability-стек и CI gates: ловить регрессии до выпускаmiddle
- От инцидента к enforcement: SLO burn до верифицированного исправления за 35 минутmiddle
- Культура, экономика и масштаб performancesenior
- At-most-once, at-least-once, exactly-once: три контракта доставкиjunior
- Три ножки сбоя — где реально происходят дубликаты и потериmiddle
- Consumer-side dedup: самый дешёвый путь к exactly-once processingmiddle
- Kafka exactly-once semantics: idempotent producer и транзакцииmiddle
- SQS visibility timeout, DLQ и outbox patternmiddle
- Exactly-once в production: impossibility-доказательство, гибридные паттерны и реальные инцидентыsenior
- Что такое OAuth и почему пароли — не ответjunior
- Authorization code flow с PKCEmiddle
- Валидация ID-токена и управление JWKS-кешемmiddle
- Ротация refresh-токенов и scope-based least privilegemiddle
- Sender-constrained токены: DPoP и mTLSsenior
- OAuth в production: audience атаки, observability и реальные провалыsenior
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.