Menu

Подъём состояния в React: общее состояние компонентов

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

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

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

Нажмите «Show» на любой панели: она откроется, а та, что была открыта, закроется. Одновременно может быть открыта только одна панель, потому что панели не решают это сами. App хранит openIndex, а каждая Panel получает только isOpen и функцию onOpen.

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

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

Откройте обе панели: они останутся открытыми вместе. Состояние в React приватно для компонента, который его объявляет, поэтому сосед не может его прочитать или сбросить. Когда двум компонентам нужно согласоваться, состояние должно жить выше их обоих.

Подъём состояния за три шага

Чтобы превратить второй пример в первый, нужно три правки.

  1. Уберите состояние из дочернего компонента. Удалите useState в Panel и читайте isOpen из пропсов. Дочерний компонент больше не решает, открыт ли он.
  2. Передайте из родителя значение и способ его изменить. Panel получает isOpen и колбэк onOpen. Дочерний компонент вызывает onOpen(), когда нажата его кнопка; что родитель с этим делает, он не знает.
  3. Добавьте состояние в общего родителя. App объявляет openIndex и превращает его в пропсы для каждой панели: isOpen={openIndex === 1} и onOpen={() => setOpenIndex(1)}.

Ближайший общий родитель это самый нижний компонент, который рендерит все компоненты, которым нужно состояние. Здесь это App. Если бы панели находились внутри компонента Faq, состояние пошло бы в Faq, а не выше.

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

Единственный источник истины

Когда две части экрана показывают один и тот же факт, храните этот факт один раз и вычисляйте из него всё остальное. Классический случай это конвертер температуры: поля Цельсия и Фаренгейта всегда должны совпадать, поэтому каждое не может хранить своё число.

Пишите в любое поле, и другое последует за ним. Состояние это один факт: число, которое пользователь ввёл последним, и шкала, в которой оно было. Другое поле вычисляется из него во время рендера, поэтому поле, в котором вы печатаете, всегда хранит ровно то, что вы ввели. Замените начальное состояние на { value: '212', scale: 'f' }, и сообщение под полями сменится на «Water boils.»

Хранить число по Цельсию и число по Фаренгейту в двух значениях состояния означало бы, что каждый обработчик должен обновлять оба, и в первый же раз, когда один обработчик забудет, поля разойдутся. Одно сохранённое значение не может противоречить самому себе.

Передача сеттера вниз

Дочерний компонент может изменить состояние родителя только через функцию, которую родитель ему даёт. Можно передать сам сеттер (onSelect={setColor}) или функцию, которая делает больше (onOpen={() => setOpenIndex(1)}). Если дать пропсу имя в стиле события, например onSelect или onChange, а не setSelected, дочерний компонент не будет знать, как родитель хранит значение, и родитель сможет позже это изменить, не трогая дочерний.

ColorPicker и Preview никогда не общаются друг с другом. Палитра сообщает о выборе наверх, App его сохраняет, и новое значение течёт вниз к обоим.

Когда не поднимать состояние

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

Пишите в поле и смотрите в консоль: при каждом нажатии клавиши рендерится только SearchBox. Теперь перенесите query в App и передайте его в SearchBox пропсами. Тогда каждое нажатие клавиши выводит ещё и ProductList, хотя списку запрос не нужен. Если бы список фильтровался по запросу, подъём был бы правильным решением, ведь тогда оба компонента зависели бы от одного значения.

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

Когда подъём заходит слишком далеко

Иногда ближайший общий родитель находится далеко вверху дерева, и значение приходится передавать через несколько компонентов, которые только передают его дальше. Это prop drilling. Несколько уровней пропсов это нормально и легко отслеживается. Когда одно и то же значение проходит через много уровней или оно нужно почти каждому компоненту (вошедший пользователь, тема, язык), читайте его через useContext, а не передавайте вручную. Контекст меняет то, как значение доходит до детей; само состояние по-прежнему живёт в одном родителе, так что оно всё равно поднято.

Часто задаваемые вопросы

Что значит подъём состояния в React?

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

Как двум соседним компонентам делить состояние в React?

Соседи не могут читать состояние друг друга. Поместите состояние в их общего родителя, передайте значение обоим, а сеттер (или обработчик вроде onChange) тому, кто его меняет. Тогда оба соседа рендерятся из одного и того же значения.

Как дочерний компонент обновляет состояние родителя?

Родитель передаёт функцию как пропс, например onSelect={setSelected} или onSelect={(id) => setSelected(id)}, а дочерний её вызывает. Состояние остаётся в родителе; дочерний компонент только просит об изменении.

Когда не стоит поднимать состояние?

Когда состояние использует только один компонент. Подъём выше, чем нужно, заставляет родителя рендериться при каждом изменении и протягивает пропсы через компоненты, которым они не нужны. Держите состояние как можно ближе к месту использования.

Чем заменить слишком высокий подъём состояния?

Если одни и те же пропсы приходится передавать через много уровней, читайте общее значение через контекст (useContext) или перестройте структуру так, чтобы компоненты, которым оно нужно, оказались ближе друг к другу.

Иллюстрация языков программирования Coddy

Учитесь программировать с Coddy

НАЧАТЬ