open atlas
↑ К треку
React с нуля до senior RCT · 06 · 01

Подъём состояния и колокация: состояние живёт у ближайшего общего владельца

Состояние живёт в нижнем компоненте, которому оно нужно. Поднимайте лишь до ближайшего общего владельца: каждый лишний уровень расширяет зону ре-рендера. «Глобально по умолчанию» делает нажатие клавиши рендером всего приложения; композиция через children изолирует состояние.

RCT Middle ◷ 17 min
Уровень
ОсновыJuniorMiddleSenior
Уже знаешь этот юнит? Пройди быструю проверку за минуту →

Страница настроек была самым медленным экраном продукта, и никто не мог объяснить почему: это была форма из девяти полей. Профайлер рассказал всё одним флейм-графом — одно нажатие клавиши в поле «отображаемое имя» ре-рендерило 1 400 компонентов, включая навигационную оболочку, бейдж уведомлений и таблицу тарифов на экране, на который пользователь даже не смотрел. Причиной была командная конвенция, записанная два года назад: «клади состояние в стор приложения, чтобы потом было доступно отовсюду». Черновое значение каждого инпута жило в корне. Каждое нажатие обновляло состояние на уровне корня, и React делал ровно то, что обещает: ре-рендерил владельца и обходил всё его поддерево. Починка заняла полдня и удалила код — черновики переехали в форму, форма опустила общие куски в самый нижний компонент, у которого реально были оба читателя, и рендер на нажатие упал с 1 400 компонентов до 12. Ничего не мемоизировали. Состояние просто переехало туда, где ему место.

К концу урока вы будете точно знать, куда перенести состояние, когда одно нажатие клавиши даёт 1 400 ре-рендеров — и почему лечение удаляет код, а не добавляет мемоизацию.

Один владелец: данные вниз, события вверх

Состояние в React локально по построению: useState принадлежит ровно одному экземпляру компонента, и этот экземпляр — единственный владелец, единственное место, где значение может измениться. Когда одно и то же изменяющееся значение нужно двум компонентам, его не дублируют (две копии обязательно разъедутся; баг-репорт звучит как «чип фильтра активен, а список не отфильтрован»). Его поднимают: находят самый нижний компонент, который является предком всех читателей — ближайшего общего владельца, — переносят состояние туда, передают значение вниз пропсами, а обработчики изменений — вниз детям, чтобы те могли запрашивать обновления. Данные текут вниз, события — вверх, и у каждого куска состояния один источник истины.

function SearchSection() {
  // Ближайший общий владелец <SearchInput> и <ResultsList>:
  // query нужен обоим, поэтому живёт здесь — и не выше.
  const [query, setQuery] = useState("");
  return (
    <section>
      <SearchInput value={query} onChange={setQuery} />
      <ResultsList query={query} />
    </section>
  );
}

function SearchInput({ value, onChange }) {
  // Контролируемый: компонент ничем не владеет; он сообщает намерение наверх.
  return <input value={value} onChange={(e) => onChange(e.target.value)} />;
}

Ключевое слово — ближайший. Подъём — это не «унести наверх», а «поднять ровно до первого общего предка и остановиться». Каждый уровень выше этой точки добавляет подписчиков, которым значение не нужно.

Зона поражения: почему высота — это решение о производительности

Когда вы видите компоненты, ре-рендерящиеся без видимой причины, первый вопрос — структурный: как высоко сидит их владелец? Когда состояние меняется, React ре-рендерит компонент-владельца и затем, по умолчанию, всё его поддерево — каждый потомок вызывается заново, читает он изменившееся значение или нет. (Ре-рендер — это дешёвая фаза render, а не мутация DOM, но вызывать 1 400 функций и согласовывать их вывод 60 раз, пока человек печатает, — не бесплатно.) Высота куска состояния, таким образом, задаёт его зону поражения: состояние в листе ре-рендерит лист; состояние в корне ре-рендерит приложение. Это и есть механизм анти-паттерна «глобально по умолчанию» — складывать всё в корневой стор или верхнеуровневый контекст не просто запутывает код: каждая запись становится рендером всего приложения, и дальше приходится отбиваться React.memo на сотнях компонентов. Колокация (colocation — хранение состояния там, где оно используется) — обратная дисциплина: заметив, что значение читает лишь одно поддерево, опустите состояние обратно вниз. Спуск состояния — чистый выигрыш: меньше рендеров, меньше протаскивания пропсов, и компонент становится удаляемым целиком.

Почему это работает

Почему React ре-рендерит всё поддерево, а не только компоненты, читающие изменившееся состояние? Потому что с пропсами «читает» — не подписка, которую React способен увидеть: любой потомок может получать значение, выведенное из этого состояния произвольным JavaScript. Единственное безопасное допущение после ре-рендера владельца — что пропсы любого ребёнка могли измениться, поэтому React перезапускает их и даёт reconciliation отбросить идентичный вывод. Точечные обновления по подписке требуют явного контракта — селекторов внешних сторов, это урок 3 этого юнита. С обычным состоянием ваш рычаг структурный: держите владельца низко.

Композиция: изоляция состояния через проп children

Есть структурный приём, который для типового случая бьёт мемоизацию: передавайте тяжёлые поддеревья как children. Дети в JSX — это элементы, объекты, создаваемые там, где JSX написан, а не там, где он рендерится. Если обёртка с состоянием получает содержимое через children, её ре-рендер переиспользует те же самые объекты-элементы, что ей передали (родитель, который их создал, не ре-рендерился). React видит идентичную ссылку на элемент и выходит досрочно, не ре-рендеря это поддерево вовсе.

function ScrollTracker({ children }) {
  const [y, setY] = useState(0); // обновляется на каждый тик скролла
  // children создал родитель, и они не менялись —
  // та же ссылка на элемент, поэтому React пропускает их ре-рендер.
  return <div onScroll={(e) => setY(e.target.scrollTop)}>
    <ProgressBar y={y} />
    {children}
  </div>;
}

// <ScrollTracker><HeavyTable rows={rows} /></ScrollTracker>
// HeavyTable НЕ ре-рендерится на скролл.

Это изоляция состояния архитектурой: высокочастотное состояние (скролл, hover, черновики ввода) получает узкий компонент для жизни, а тяжёлое содержимое проходит сквозь него нетронутым. Компромисс: подъём и колокация — это рефакторинг при каждом сдвиге требований: значение, нужное сегодня одному компоненту, завтра получает второго читателя, и его придётся переносить. Это трение реально, и это честная цена контроля зоны поражения; команды, отказывающиеся её платить, сползают обратно к «глобально по умолчанию» и платят рендерами. Сценарий отказа: дублирование состояния вместо подъёма («заведу ещё и локальную копию») — два владельца, неизбежное расхождение и класс багов, где UI противоречит сам себе.

Викторина

Двум компонентам-сиблингам нужно читать и обновлять одно значение фильтра. Каков правильный ход и почему именно туда?

Викторина

Обёртка с быстро меняющимся состоянием скролла получает тяжёлую таблицу через проп children. Почему таблица не ре-рендерится при обновлении состояния обёртки?

Выбери лучший вариант

Значение фильтра читают и обновляют два компонента-сиблинга на одной странице. Где должен жить единственный useState?

Вспомните перед уходом
  1. 01
    Объясните механизм, связывающий высоту куска состояния со стоимостью рендеринга, и что операционно значит «ближайший общий владелец».
  2. 02
    Почему передача дорогого поддерева как children изолирует его от ре-рендеров обёртки с состоянием, и когда это лучше React.memo?
Итог

У состояния в React ровно один владелец, и архитектура — это решение, насколько высоко он сидит. По умолчанию — колокация: состояние живёт в самом нижнем компоненте, которому нужно. Когда значение делят сиблинги, его поднимают: найдите ближайшего общего предка всех читателей, перенесите туда единственный useState, значение передавайте вниз пропсами, намерения — вверх обработчиками. Один источник истины, без дублирования и синхронизирующих эффектов. Модель стоимости, из-за которой высота важна: запись в состояние ре-рендерит владельца и всё его поддерево, потому что React не может знать, какие потомки читают значение через пропсы — каждый уровень выше ближайшего общего владельца добавляет компоненты, ре-рендерящиеся впустую. «Глобально по умолчанию» — эта ошибка, возведённая в правило: корневой стор для черновиков ввода превратил одно нажатие клавиши в рендер 1 400 компонентов в истории из Hook, и именно спуск состояния — не мемоизация — сократил его до 12. Структурное дополнение — композиция: обёртка, владеющая быстрым состоянием и получающая тяжёлое содержимое через children, ре-рендерится одна, потому что элементы children создал родитель, ссылка не изменилась, и React выходит из них досрочно. У подъёма честная цена — состояние переезжает при смене читателей, и этот рефакторинг платится снова и снова, — но альтернатива — платить зоной поражения на каждую запись. Опускайте состояние при сужении читателей; поднимайте, только когда они действительно расходятся. Теперь, когда вы увидите в профайлере флейм-граф из компонентов, которые не должны ре-рендериться, первый вопрос — структурный: как высоко сидит владелец, и можно ли его опустить?

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 6 завершено
Связанные уроки

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources2
expand
  1. 01
  2. 02

Trademarks belong to their respective owners. Editorial reference only.