Суть Читайте реальные сниппеты React — баг индексного ключа при reorder, побочный эффект в render-фазе, неправильное разделение приоритетов и сорванный bailout — и выбирайте сеньорский фикс.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на senior-высоте — в орбите
◷ 14 min
Баги fiber читаются в коде и в Profiler, а не в документации. Прочитайте каждый сниппет, предскажите, как реконсилер его обработает, и выберите изменение, которое сеньор сделал бы первым.
Цель
Отработайте цикл, который вы запускаете в каждом инциденте производительности или корректности React: прочитать компонент, предсказать, как reconciliation, разделение render/commit и планировщик lanes его обработают, и выбрать самый рычажный фикс.
Сниппет 1 — переупорядочиваемый список
function TodoList({ todos }) { // todos можно переупорядочивать перетаскиванием, у каждой строки // есть неуправляемый <input>, куда пользователь печатает заметки return ( <ul> {todos.map((todo, i) => ( <li key={i}> <span>{todo.title}</span> <input type="text" placeholder="notes…" /> </li> ))} </ul> );}
Викторина
Completed
Пользователь печатает заметки в строку 3, затем перетаскивает строку 1 в самый низ. Заметки как будто прыгают на неправильную строку. В чём баг и исправление?
Heads-up Неуправляемые input корректно переупорядочиваются со стабильным ключом. Дефект — позиционный ключ, а не отсутствие пропа value; сделать его управляемым — другое изменение, требующее ещё и состояния на строку.
Heads-up Никакой эффект не исправит несовпавшую идентичность; input уже привязаны к неправильным fiber к моменту запуска любого эффекта. Фикс выше по потоку, в ключе, чтобы reconciliation спарил fiber с правильными todo.
Heads-up Мемоизация JSX не меняет, как reconciliation сопоставляет детей — он всё равно ключует по i. Проблема идентичности — сам ключ, а не частота пересборки массива.
Сниппет 2 — работа в теле функции
function Chart({ points }) { // строим производный датасет и логируем аналитическое событие const series = expensiveTransform(points); analytics.track('chart_rendered', { count: series.length }); return <svg>{series.map(p => <Dot key={p.id} {...p} />)}</svg>;}
Викторина
Completed
Какая строка — латентный баг при concurrent rendering в React 18 и почему?
Heads-up Чистые вычисления в render допустимы; если они медленные, их мемоизируют через useMemo, но это не баг корректности. Баг корректности — нечистый вызов аналитики, дающий наблюдаемые эффекты при каждом повторе.
Heads-up Спред пропсов нормален и не влияет на идентичность reconciliation, которая управляется ключом. К нарушению чистоты рендера он отношения не имеет.
Heads-up React 18 явно не гарантирует один вызов на обновление — рендер может быть переигран или отброшен. Именно поэтому побочные эффекты в теле функции небезопасны.
Ввод дёргается, хотя дорогой фильтр внутри startTransition. Что не так с этим разделением приоритетов?
Heads-up startTransition может обернуть любое число обновлений. Проблема — какие обновления туда относятся: срочное значение input не должно откладываться, а дорогое обновление списка — должно.
Heads-up startTransition как раз помещает обновление в time-sliced transition-lane. Дело не в выборе API; срочное обновление query ошибочно помещено в низкоприоритетную lane.
Heads-up isPending выводится React из ожидающего transition; setResults не гейтят по нему. Спиннер рендерится нормально — реальный дефект в отложенном значении input.
Каждый Row перерендеривается всякий раз, когда перерендеривается List, хотя React.memo оборачивает Row и items не менялись. Почему и какой минимальный фикс?
Heads-up React.memo работает на любом функциональном компоненте независимо от того, что он рендерит. Его срывает нестабильный inline-проп-стрелка, а не тип элемента.
Heads-up Ключ корректен и обязателен — именно он даёт каждой строке стабильную идентичность. Его удаление вызвало бы предупреждения и худшую reconciliation. Срыв — inline-функция-проп, а не ключ.
Heads-up Row мемоизирован по собственным пропсам (item, onSelect), а не по массиву items родителя. Даже если items новый, каждая ссылка item может быть стабильной; настоящий виновник — свежая стрелка onSelect, создаваемая на строку.
Итог
Четыре чтения на уровне fiber: индексный ключ следует за позицией, поэтому неуправляемое состояние на fiber дрейфует к неправильному элементу при reorder — используйте стабильный id; побочный эффект в теле функции выполняется непредсказуемое число раз при повторе — перенесите его в useEffect или в событие; срочное значение input должно оставаться вне startTransition, а внутрь идёт только дорогое обновление; и inline-стрелка-проп — свежая ссылка каждый рендер, тихо срывающая React.memo. Прочитайте код, предскажите, как реконсилер его обработает, исправьте идентичность или ссылку прежде, чем тянуться к любому другому рычагу.