Поширення стану між компонентами

Іноді вам потрібно, щоб стан двох компонентів завжди змінювався разом. Для цього видаліть стан з обох компонентів, перенесіть його до їхнього найближчого спільного батьківського компонента і потім передайте вниз до них через пропси. Це називається підняттям стану вгору (lifting state up), і це одна з найпоширеніших речей, які ви будете робити під час написання React-коду.

Ви вивчите

  • Як поширювати стан між компонентами підняттям його вгору
  • Що таке контрольовані та неконтрольовані компоненти

Підняття стану на прикладі

У цьому прикладі батьківський компонент 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 кожного Panel дорівнює false, тому обидва мають згорнутий вигляд

Та сама діаграма, що й попередня, з виділеним isActive першого дочірнього компонента Panel, що вказує на натискання зі значенням isActive, встановленим у true. Другий компонент Panel все ще містить значення false.
Та сама діаграма, що й попередня, з виділеним isActive першого дочірнього компонента Panel, що вказує на натискання зі значенням isActive, встановленим у true. Другий компонент Panel все ще містить значення false.

Натискання на будь-яку з кнопок Panel призведе до оновлення стану isActive тільки цієї Panel

Але тепер припустимо, що ви хочете змінити це так, щоб тільки одна панель була розгорнута в будь-який момент часу. За цього дизайну розгортання другої панелі повинно згорнути першу. Як би ви це зробили?

Щоб скоординувати ці дві панелі, вам потрібно “підняти їхній стан вгору” до батьківського компонента в три кроки:

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

Це дасть компоненту Accordion змогу скоординувати обидва компоненти Panel та розгортати тільки один щоразу.

Крок 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 має відстежувати, яка панель є активною. Замість значення типу boolean, можна використовувати число як індекс активного компонента Panel для змінної стану:

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

Коли activeIndex дорівнює 0, перша панель буде активною, а коли 1 — то друга.

Натискання кнопки “Показати” на одному із Panel має змінити активний індекс в 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 як обробник події клацання:

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>
  );
}

Підняття стану вгору завершено! Переміщення стану до спільного батьківського компонента дозволяє вам скоординувати дві панелі. Використання індексу для активності замість двох прапорців “активно” гарантує, що тільки одна панель буде активною в певний момент. А передавання обробника подій вниз до дочірнього компонента дає йому змогу змінювати батьківський стан.

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

Початково Accordion має activeIndex зі значенням 0, тому перший компонент Panel отримує isActive = true

Така сама діаграма, що й попередня, тільки підсвічено значення activeIndex батьківського компонента, що вказує на клік із значенням, зміненим на одиницю. Шлях до обох дочірніх компонентів Panel також підсвічений, а значення isActive, передане до кожного дочірнього компонента, є протилежним: false для першого Panel та true для другого.
Така сама діаграма, що й попередня, тільки підсвічено значення activeIndex батьківського компонента, що вказує на клік із значенням, зміненим на одиницю. Шлях до обох дочірніх компонентів Panel також підсвічений, а значення isActive, передане до кожного дочірнього компонента, є протилежним: false для першого Panel та true для другого.

Коли в Accordion стан activeIndex змінюється на 1, друга Panel отримує isActive = true

Занурення

Контрольовані та неконтрольовані компоненти

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

На противагу, ви можете сказати, що компонент є “контрольованим”, коли важлива інформація в ньому керується за допомогою пропсів, а не його власним локальним станом. Це дає батьківському компоненту змогу повністю визначити його поведінку. Останній компонент Panel із пропом isActive контролюється компонентом Accordion.

Неконтрольовані компоненти легше використовувати всередині батьківських, оскільки вони вимагають менше налаштувань. Але вони менш гнучкі, коли ви хочете скоординувати їх разом. Контрольовані компоненти є максимально гнучкими, але вони вимагають від батьківських компонентів повного налаштування за допомогою пропсів.

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

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

Єдине джерело правди для кожного стану

У React-застосунку багато компонентів матимуть їхній власний стан. Деякий стан може “жити” близько до листкових компонентів (компоненти, що знаходяться внизу дерева), як-от поля вводу. Інші стани можуть “жити” ближче до вершини застосунку. Для прикладу, навіть клієнтські бібліотеки маршрутизації зазвичай реалізовані за допомогою зберігання поточного маршруту в стані React та передають його вниз через пропси!

Для кожної унікальної частинки стану ви оберете компонент, який “володіє” ним. Цей принцип також відомий як “єдине джерело правди”. Це не означає, що весь стан знаходиться в одному місці, це означає, що для кожної частини стану є певний компонент, який утримує ту частину інформації. Замість дублювання спільного стану між компонентами, підніміть його вгору до їхнього спільного батька та передайте його вниз до дочірніх компонентів, де він потрібний.

Ваш застосунок буде змінюватися в міру того, як ви працюєте над ним. Часто буває так, що ви переміщуєте стан вниз або назад вгору, поки намагаєтеся з’ясувати, де кожен шматок стану “живе”. Це все частина процесу!

Щоб побачити, як це відчувається на практиці з кількома іншими компонентами, прочитайте “Thinking in React”.

Підсумок

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

Завдання 1 із 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>
  );
}