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

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

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

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

У цьому прикладі батьківський Accordion компонент рендерить два окремих Panels:

  • 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)}>
          Show
        </button>
      )}
    </section>
  );
}

export default function Accordion() {
  return (
    <>
      <h2>Almaty, Kazakhstan</h2>
      <Panel title="About">
        With a population of about 2 million, Almaty is Kazakhstan's largest city. From 1929 to 1997, it was its capital city.
      </Panel>
      <Panel title="Etymology">
        The name comes from <span lang="kk-KZ">алма</span>, the Kazakh word for "apple" and is often translated as "full of apples". In fact, the region surrounding Almaty is thought to be the ancestral home of the apple, and the wild <i lang="la">Malus sieversii</i> is considered a likely candidate for the ancestor of the modern domestic apple.
      </Panel>
    </>
  );
}
h3, p { margin: 5px 0px; }
.panel {
  padding: 10px;
  border: 1px solid #aaa;
}

Зверніть увагу, що натискання кнопки однієї панелі не впливає на іншу панель - вони незалежні.

Diagram showing a tree of three components, one parent labeled Accordion and two children labeled Panel. Both Panel components contain isActive with value false.

Початково стан кожного Panel з isActive є false, тому вони обидва виглядають згорнутими

The same diagram as the previous, with the isActive of the first child Panel component highlighted indicating a click with the isActive value set to true. The second Panel component still contains value false.

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

.

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

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

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

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

Крок 1: Видалення стану з дочірніх компонентів

Ви передасте керування Panel батьківському компоненту isActive. Це означає, що батьківський компонент передасть 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>Almaty, Kazakhstan</h2>
      <Panel title="About" isActive={true}>
        With a population of about 2 million, Almaty is Kazakhstan's largest city. From 1929 to 1997, it was its capital city.
      </Panel>
      <Panel title="Etymology" isActive={true}>
        The name comes from <span lang="kk-KZ">алма</span>, the Kazakh word for "apple" and is often translated as "full of apples". In fact, the region surrounding Almaty is thought to be the ancestral home of the apple, and the wild <i lang="la">Malus sieversii</i> is considered a likely candidate for the ancestor of the modern domestic apple.
      </Panel>
    </>
  );
}

function Panel({ title, children, isActive }) {
  return (
    <section className="panel">
      <h3>{title}</h3>
      {isActive ? (
        <p>{children}</p>
      ) : (
        <button onClick={() => setIsActive(true)}>
          Show
        </button>
      )}
    </section>
  );
}
h3, p { margin: 5px 0px; }
.panel {
  padding: 10px;
  border: 1px solid #aaa;
}

Спробуйте відредагувати жорстко закодовані значення isActive у компоненті Accordion і подивіться результат на екрані.

Крок 3: Додавання стану до спільного батька

Підняття стану вгору часто змінює природу того, що ви зберігаєте як стан.

У цьому випадку одночасно має бути активною лише одна панель. Це означає, що спільний батьківський компонент Accordion повинен відстежувати яка з панелей є активною. Замість булевого значення , він може використовувати число як індекс активної 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>Almaty, Kazakhstan</h2>
      <Panel
        title="About"
        isActive={activeIndex === 0}
        onShow={() => setActiveIndex(0)}
      >
        With a population of about 2 million, Almaty is Kazakhstan's largest city. From 1929 to 1997, it was its capital city.
      </Panel>
      <Panel
        title="Etymology"
        isActive={activeIndex === 1}
        onShow={() => setActiveIndex(1)}
      >
        The name comes from <span lang="kk-KZ">алма</span>, the Kazakh word for "apple" and is often translated as "full of apples". In fact, the region surrounding Almaty is thought to be the ancestral home of the apple, and the wild <i lang="la">Malus sieversii</i> is considered a likely candidate for the ancestor of the modern domestic apple.
      </Panel>
    </>
  );
}

function Panel({
  title,
  children,
  isActive,
  onShow
}) {
  return (
    <section className="panel">
      <h3>{title}</h3>
      {isActive ? (
        <p>{children}</p>
      ) : (
        <button onClick={onShow}>
          Show
        </button>
      )}
    </section>
  );
}
h3, p { margin: 5px 0px; }
.panel {
  padding: 10px;
  border: 1px solid #aaa;
}

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

Diagram showing a tree of three components, one parent labeled Accordion and two children labeled Panel. Accordion contains an activeIndex value of zero which turns into isActive value of true passed to the first Panel, and isActive value of false passed to the second Panel.

Спочатку Accordion activeIndex є 0, тому перший Panel отримує isActive = true

The same diagram as the previous, with the activeIndex value of the parent Accordion component highlighted indicating a click with the value changed to one. The flow to both of the children Panel components is also highlighted, and the isActive value passed to each child is set to the opposite: false for the first Panel and true for the second one.

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

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

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

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

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

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

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

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

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

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

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

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

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

Синхронізовані входи

Ці два входи є незалежними. Зробіть так, щоб вони синхронізувалися: редагування одного входу має оновити інший вхід тим самим текстом, і навпаки.

Вам потрібно підняти їхній стан до батьківського компонента.

import { useState } from 'react';

export default function SyncedInputs() {
  return (
    <>
      <Input label="First input" />
      <Input label="Second input" />
    </>
  );
}

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

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

  return (
    <label>
      {label}
      {' '}
      <input
        value={text}
        onChange={handleChange}
      />
    </label>
  );
}
input { margin: 5px; }
label { display: block; }

Перемістіть змінну стану text у батьківський компонент разом з обробником handleChange. Потім передайте їх як пропси обом компонентам Input. Це забезпечить їхню синхронізацію.

import { useState } from 'react';

export default function SyncedInputs() {
  const [text, setText] = useState('');

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

  return (
    <>
      <Input
        label="First input"
        value={text}
        onChange={handleChange}
      />
      <Input
        label="Second input"
        value={text}
        onChange={handleChange}
      />
    </>
  );
}

function Input({ label, value, onChange }) {
  return (
    <label>
      {label}
      {' '}
      <input
        value={value}
        onChange={onChange}
      />
    </label>
  );
}
input { margin: 5px; }
label { display: block; }

Фільтрування списку

У цьому прикладі SearchBar має власний стан query, який контролює введення тексту. Його батьківський компонент FilterableList відображає List елементів, але не враховує пошуковий запит.

Використовуйте функцію filterItems(foods, query) для фільтрації списку відповідно до пошукового запиту. Щоб перевірити ваші зміни, переконайтеся, що введення "s" у вхідних даних фільтрує список до "Суші", "Шашлик" і "Дім сам".

Зверніть увагу, що filterItems вже реалізовано та імпортовано, тому вам не потрібно писати його самостійно!

Вам потрібно видалити стан запиту та обробник handleChange з SearchBar і перемістити їх до FilterableList. Потім передайте їх до SearchBar як query та onChange props.

import { useState } from 'react';
import { foods, filterItems } from './data.js';

export default function FilterableList() {
  return (
    <>
      <SearchBar />
      <hr />
      <List items={foods} />
    </>
  );
}

function SearchBar() {
  const [query, setQuery] = useState('');

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

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

function List({ items }) {
  return (
    <table>
      <tbody>
        {items.map(food => (
          <tr key={food.id}>
            <td>{food.name}</td>
            <td>{food.description}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}
export function filterItems(items, query) {
  query = query.toLowerCase();
  return items.filter(item =>
    item.name.split(' ').some(word =>
      word.toLowerCase().startsWith(query)
    )
  );
}

export const foods = [{
  id: 0,
  name: 'Sushi',
  description: 'Sushi is a traditional Japanese dish of prepared vinegared rice'
}, {
  id: 1,
  name: 'Dal',
  description: 'The most common way of preparing dal is in the form of a soup to which onions, tomatoes and various spices may be added'
}, {
  id: 2,
  name: 'Pierogi',
  description: 'Pierogi are filled dumplings made by wrapping unleavened dough around a savoury or sweet filling and cooking in boiling water'
}, {
  id: 3,
  name: 'Shish kebab',
  description: 'Shish kebab is a popular meal of skewered and grilled cubes of meat.'
}, {
  id: 4,
  name: 'Dim sum',
  description: 'Dim sum is a large range of small dishes that Cantonese people traditionally enjoy in restaurants for breakfast and lunch'
}];

Підніміть стан запиту у компонент FilterableList. Викличте filterItems(foods, query), щоб отримати відфільтрований список і передайте його до List. Тепер зміна вхідних даних запиту відображається у списку:

import { useState } from 'react';
import { foods, filterItems } from './data.js';

export default function FilterableList() {
  const [query, setQuery] = useState('');
  const results = filterItems(foods, query);

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

  return (
    <>
      <SearchBar
        query={query}
        onChange={handleChange}
      />
      <hr />
      <List items={results} />
    </>
  );
}

function SearchBar({ query, onChange }) {
  return (
    <label>
      Search:{' '}
      <input
        value={query}
        onChange={onChange}
      />
    </label>
  );
}

function List({ items }) {
  return (
    <table>
      <tbody> 
        {items.map(food => (
          <tr key={food.id}>
            <td>{food.name}</td>
            <td>{food.description}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}
export function filterItems(items, query) {
  query = query.toLowerCase();
  return items.filter(item =>
    item.name.split(' ').some(word =>
      word.toLowerCase().startsWith(query)
    )
  );
}

export const foods = [{
  id: 0,
  name: 'Sushi',
  description: 'Sushi is a traditional Japanese dish of prepared vinegared rice'
}, {
  id: 1,
  name: 'Dal',
  description: 'The most common way of preparing dal is in the form of a soup to which onions, tomatoes and various spices may be added'
}, {
  id: 2,
  name: 'Pierogi',
  description: 'Pierogi are filled dumplings made by wrapping unleavened dough around a savoury or sweet filling and cooking in boiling water'
}, {
  id: 3,
  name: 'Shish kebab',
  description: 'Shish kebab is a popular meal of skewered and grilled cubes of meat.'
}, {
  id: 4,
  name: 'Dim sum',
  description: 'Dim sum is a large range of small dishes that Cantonese people traditionally enjoy in restaurants for breakfast and lunch'
}];