Совместное использование состояния между компонентами

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

You will learn

  • Как использовать одно состояние между компонентами, подняв его вверх
  • Что такое управляемые и неуправляемые компоненты

Подъём состояния на примере

В этом примере родительский компонент Accordion рендерит два отдельных компонента Panel:

  • Accordion
    • Panel
    • Panel

Каждый компонент Panel имеет булевое состояние isActive, которое определяет, будет ли его содержимое видимым.

Нажмите кнопку Показать на обеих панелях:

import { useState } from 'react';

function Panel({ title, children }) {
  const [isActive, setIsActive] = useState(false);
  return (
    <section className="panel">
      <h3>{title}</h3>
      {isActive ? (
        <p>{children}</p>
      ) : (
        <button onClick={() => setIsActive(true)}>
          Показать
        </button>
      )}
    </section>
  );
}

export default function Accordion() {
  return (
    <>
      <h2>Алматы, Казахстан</h2>
      <Panel title="Подробнее">
        Алматы с населением около 2 миллионов человек является крупнейшим городом Казахстана. С 1929 по 1997 год этот город был столицей.
      </Panel>
      <Panel title="Этимология">
        Название происходит от <span lang="kk-KZ">алма</span>, казахского слова, означающего "яблоко", и часто переводится как "полное яблок". На самом деле, регион, прилегающий к Алматы, считается прародиной яблони, а дикий <i lang="la">Malus sieversii</i> считается вероятным кандидатом на роль предка современного домашнего яблока.
      </Panel>
    </>
  );
}

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

Диаграмма, показывающая дерево из трех компонентов: один родительский компонент с названием Accordion и два дочерних компонента с названием Panel. Оба компонента Panel содержат состояние isActive со значением false.
Диаграмма, показывающая дерево из трех компонентов: один родительский компонент с названием Accordion и два дочерних компонента с названием Panel. Оба компонента Panel содержат состояние isActive со значением false.

Изначально, каждая панель имеет состояние isActive в значении false, поэтому они обе отображаются свернутыми

Диаграмма такая же, как и предыдущая, но выделено состояние isActive у первого дочернего компонента Panel, это показывает, что был совершен клик и значение isActive установлено в true. Второй компонент Panel по-прежнему содержит значение false.
Диаграмма такая же, как и предыдущая, но выделено состояние isActive у первого дочернего компонента Panel, это показывает, что был совершен клик и значение isActive установлено в true. Второй компонент Panel по-прежнему содержит значение false.

Нажатие кнопки на одной из панелей приведет к обновлению состояния isActive только этой панели

А теперь предположим, что вы хотите изменить поведение так, чтобы в любой момент времени была раскрыта только одна панель. В таком случае раскрытие второй панели должно привести к сворачиванию первой. Как бы вы это сделали?

Чтобы согласовать поведение этих двух панелей, вам нужно “поднять их состояние” в родительский компонент в три шага:

  1. Удалите состояние из дочерних компонентов.
  2. Передайте данные хардкодом из общего родителя.
  3. Добавьте состояние в общего родителя и передайте его вместе с обработчиками событий.

Это позволит компоненту Accordion управлять обеими панелями и раскрывать только по одной за раз.

Шаг 1: Удалить состояние из дочерних компонентов

Вы передадите управление значением isActive родительскому компоненту Panel. Это означает, что родительский компонент будет передавать значение isActive через проп, вместо того, чтобы хранить это состояние в Panel. Начните с удаления этой строки из компонента Panel:

const [isActive, setIsActive] = useState(false);

И вместо нее, добавьте isActive в список пропсов Panel:

function Panel({ title, children, isActive }) {

Теперь родитель компонента Panel может управлять isActive передавая его вниз как проп. С другой стороны, компонент Panel теперь не имеет контроля над значением isActive—теперь оно зависит от родительского компонента!

Шаг 2: Передать данные хардкодом из общего родителя

Чтобы поднять состояние, вам необходимо определить ближайшего общего родителя для обоих дочерних компонентов, которые вы хотите скоординировать:

  • Accordion (ближайший общий родитель)
    • Panel
    • Panel

В данном примере это компонент Accordion. Поскольку он находится выше обеих панелей и может контролировать их пропсы, он станет “источником истины”, чтобы определить, какая панель сейчас активна. Давайте сделаем так, чтобы Accordion передавал значения isActive хардкодом (например, true) обеим панелям:

import { useState } from 'react';

export default function Accordion() {
  return (
    <>
      <h2>Алматы, Казахстан</h2>
      <Panel title="Подробнее" isActive={true}>
        Алматы с населением около 2 миллионов человек является крупнейшим городом Казахстана. С 1929 по 1997 год этот город был столицей.
      </Panel>
      <Panel title="Этимология" isActive={true}>
        Название происходит от <span lang="kk-KZ">алма</span>, казахского слова, означающего "яблоко", и часто переводится как "полное яблок". На самом деле, регион, прилегающий к Алматы, считается прародиной яблони, а дикий <i lang="la">Malus sieversii</i> считается вероятным кандидатом на роль предка современного домашнего яблока.
      </Panel>
    </>
  );
}

function Panel({ title, children, isActive }) {
  return (
    <section className="panel">
      <h3>{title}</h3>
      {isActive ? (
        <p>{children}</p>
      ) : (
        <button onClick={() => setIsActive(true)}>
          Показать
        </button>
      )}
    </section>
  );
}

Попробуйте изменить хардкод значение isActive в компоненте Accordion и обратите внимание как это повлияет на результат.

Шаг 3: Добавить состояние в общего родителя

Подъём состояния часто приводит к изменению сущности хранимого состояния.

В нашем случае, только одна панель должна быть активна одновременно. Это означает, что общему родителю Accordion нужно следить за тем какая панель активна в данный момент. Вместо булевого значения в состоянии можно хранить число, которое будет означать индекс активной панели:

const [activeIndex, setActiveIndex] = useState(0);

Когда activeIndex равен 0, активна первая панель, а когда равен 1, активна вторая.

Нажатие на кнопку “Показать” в любой из панелей должно изменять индекс активной панели в Accordion. Panel не может напрямую установить состояние activeIndex, потому что оно определено внутри Accordion. Компоненту Accordion необходимо явно разрешить компоненту Panel изменять свое состояние, передав обработчик события как проп:

<>
<Panel
isActive={activeIndex === 0}
onShow={() => setActiveIndex(0)}
>
...
</Panel>
<Panel
isActive={activeIndex === 1}
onShow={() => setActiveIndex(1)}
>
...
</Panel>
</>

Элемент <button> внутри компонента Panel теперь будет использовать проп onShow в качестве обработчика события click:

import { useState } from 'react';

export default function Accordion() {
  const [activeIndex, setActiveIndex] = useState(0);
  return (
    <>
      <h2>Алматы, Казахстан</h2>
      <Panel
        title="Подробнее"
        isActive={activeIndex === 0}
        onShow={() => setActiveIndex(0)}
      >
        Алматы с населением около 2 миллионов человек является крупнейшим городом Казахстана. С 1929 по 1997 год этот город был столицей.
      </Panel>
      <Panel
        title="Этимология"
        isActive={activeIndex === 1}
        onShow={() => setActiveIndex(1)}
      >
        Название происходит от <span lang="kk-KZ">алма</span>, казахского слова, означающего "яблоко", и часто переводится как "полное яблок". На самом деле, регион, прилегающий к Алматы, считается прародиной яблони, а дикий <i lang="la">Malus sieversii</i> считается вероятным кандидатом на роль предка современного домашнего яблока.
      </Panel>
    </>
  );
}

function Panel({
  title,
  children,
  isActive,
  onShow
}) {
  return (
    <section className="panel">
      <h3>{title}</h3>
      {isActive ? (
        <p>{children}</p>
      ) : (
        <button onClick={onShow}>
          Показать
        </button>
      )}
    </section>
  );
}

Подъём состояния завершен! Переместив состояние в общий родительский компонент нам удалось скоординировать две панели. Использование индекса активной панели вместо двух флагов isActive гарантирует нам, что будет активна только одна панель одновременно. А передав обработчики событий дочерним компонентам мы позволили им управлять состоянием родителя.

На диаграмме изображено дерево компонентов, один родительский компонент с названием Accordion и два дочерних с названием Panel. Accordion содержит переменную activeIndex со значением ноль, которая преобразуется в isActive со значением true для первой Panel, и в isActive со значением false для второй Panel.
На диаграмме изображено дерево компонентов, один родительский компонент с названием Accordion и два дочерних с названием Panel. Accordion содержит переменную activeIndex со значением ноль, которая преобразуется в isActive со значением true для первой Panel, и в isActive со значением false для второй Panel.

В начале, activeIndex в компоненте Accordion имеет значение 0, поэтому первая панель получает isActive = true

Та же диаграмма, что и предыдущая, с выделенным значением активного индекса родительского компонента Accordion, указывающим на клик со значением, измененным на единицу. Стрелки к обоим дочерним компонентам Panel также выделены, а значение isActive, передаваемое каждому дочернему элементу, установлено в противоположное значение: false для первой панели и true для второй.
Та же диаграмма, что и предыдущая, с выделенным значением активного индекса родительского компонента Accordion, указывающим на клик со значением, измененным на единицу. Стрелки к обоим дочерним компонентам Panel также выделены, а значение isActive, передаваемое каждому дочернему элементу, установлено в противоположное значение: false для первой панели и true для второй.

Когда состояние activeIndex в Accordion становится 1, вторая Panel получает isActive = true вместо первой

Deep Dive

Управляемые и неуправляемые компоненты

Компоненты, у которых есть внутреннее состояние, обычно называют “неуправляемыми”. Например, оригинальный компонент Panel с переменной состояния isActive является неуправляемым, потому что его родитель не может повлиять на то, активна ли панель.

Напротив, компонент называют “управляемым”, если важная информация зависит от пропов, а не от внутреннего состояния. Это позволяет родительскому компоненту полностью контролировать его поведение. После наших изменений, компонентом Panel управляет компонент Accordion с помощью пропа isActive.

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

На практике, “управляемые” и “неуправляемые” это не строгие технические определения—обычно компоненты имеют и внутреннее состояние и пропсы. Однако, важно понимать как устроены компоненты и какие возможности они предоставляют.

Когда пишите компонент, подумайте какая информация в нем должна быть управляема (через пропсы), а какая неуправляема (через состояние). Но вы в любой момент можете передумать и сделать рефакторинг позже.

Единый источник истины для каждого состояния

В приложении на React у многих компонентов будет свое собственное состояние. Какое-то состояние может “жить” близко к листовым компонентам (компоненты внизу дерева), например, текстовое поле. Другое состояние может “жить” ближе к корню приложения. Например, даже при реализации библиотеки клиентской маршрутизации, текущий путь обычно сохраняют в состояние React и передают его вниз через пропсы.

Для каждого уникального кусочка состояния, вы будете выбирать, какой компонент будет им “владеть”. Этот принцип также известен как наличие “единого источника истины”. Это не означает, что все состояние React находится в одном месте—для каждой части состояния существует конкретный компонент, который хранит эту информацию. Вместо дублирования общего состояния между компонентами, поднимите его в общий родительский компонент и “пробросьте” его дочерним компонентам, которым он необходим.

Ваше приложение будет меняться по мере работы над ним. Это нормально, что вы будете перемещать состояние вниз или обратно вверх, пока разбираетесь где “живет” каждая часть состояния. Это все часть процесса!

Чтобы увидеть, как это работает на практике с другими компонентами, прочитайте статью Мышление в React..

Recap

  • Если вы хотите скоординировать два компонента, переместите их состояние к общему родительскому компоненту.
  • Затем передайте информацию через пропсы из общего родительского компонента.
  • Наконец, пробросьте обработчики событий, чтобы дочерние компоненты могли изменять состояние родительского компонента.
  • Полезно рассматривать компоненты как “управляемые” (управляемые пропами) или “неуправляемые” (управляемые состоянием).

Challenge 1 of 2:
Синхронизированные поля ввода

Эти два поля ввода независимы. Синхронизируйте их: редактирование одного поля должно обновлять другое поле с тем же текстом, и наоборот.

import { useState } from 'react';

export default function SyncedInputs() {
  return (
    <>
      <Input label="Первое поле" />
      <Input label="Второе поле" />
    </>
  );
}

function Input({ label }) {
  const [text, setText] = useState('');

  function handleChange(e) {
    setText(e.target.value);
  }

  return (
    <label>
      {label}
      {' '}
      <input
        value={text}
        onChange={handleChange}
      />
    </label>
  );
}