Збереження та скидання стану

Стан ізольований між компонентами. React відстежує, який стан належить якому компоненту, виходячи з їх місця в дереві інтерфейсу користувача. Ви можете керувати тим, коли зберігати стан, а коли скидати його між рендерами.

Ви вивчите

  • Коли React зберігає або скидає стан
  • Як примусити React скинути стан компоненти
  • Як ключі та типи впливають на збереження стану

Стан прив’язаний до позиції у дереві рендерингу

React будує дерева рендерингу для структури компонентів у вашому інтерфейсі.

Коли ви надаєте компоненту стан, ви можете подумати, що стан “живе” всередині компонента. Але насправді стан зберігається всередині React. React пов’язує кожен фрагмент стану, який він утримує, з відповідним компонентом, по тому де цей компонент знаходиться в дереві рендерингу.

В даному прикладі використовується тільки один JSX тег <Counter />, але він рендериться в двох різних позиціях.

import { useState } from 'react';

export default function App() {
  const counter = <Counter />;
  return (
    <div>
      {counter}
      {counter}
    </div>
  );
}

function Counter() {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Додати один
      </button>
    </div>
  );
}

Ось як вони відображаються у вигляді дерева:

Діаграма дерева React-компонентів. Кореневий вузол позначений як 'div' та має двоє дочірніх компонентів. Кожний дочірній компонент названий 'Counter' і обидва містять бульбашку стану з назвою 'count' із значенням 0.
Діаграма дерева React-компонентів. Кореневий вузол позначений як 'div' та має двоє дочірніх компонентів. Кожний дочірній компонент названий 'Counter' і обидва містять бульбашку стану з назвою 'count' із значенням 0.

React дерево

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

В React, кожен компонент на екрані має повністю ізольований стан. Для прикладу, якщо ви рендерите два компоненти Counter поруч, кожен з них отримає свої власні, незалежні стани score та hover.

Спробуйте натиснути на обидва лічильника і ви помітите, що вони не впливають один на одного:

import { useState } from 'react';

export default function App() {
  return (
    <div>
      <Counter />
      <Counter />
    </div>
  );
}

function Counter() {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Додати один
      </button>
    </div>
  );
}

Як ви можете бачити, коли один лічильник оновлюється, тільки стан тієї компоненти оновлюється:

Діаграма дерева React-компонентів. Кореневий вузол позначений як 'div' та має двоє дочірніх компонентів. Лівий нащадок має назву 'Counter' та містить бульбашку стану з назвою 'count' із значенням 0. Правий нащадок називається 'Counter' та містить бульбашку стану з назвою 'count' із значенням 1. Бульбашка стану правого нащадка підсвічена жовтим, щоб показати, що його значення було оновлено.
Діаграма дерева React-компонентів. Кореневий вузол позначений як 'div' та має двоє дочірніх компонентів. Лівий нащадок має назву 'Counter' та містить бульбашку стану з назвою 'count' із значенням 0. Правий нащадок називається 'Counter' та містить бульбашку стану з назвою 'count' із значенням 1. Бульбашка стану правого нащадка підсвічена жовтим, щоб показати, що його значення було оновлено.

Оновлення стану

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

import { useState } from 'react';

export default function App() {
  const [showB, setShowB] = useState(true);
  return (
    <div>
      <Counter />
      {showB && <Counter />} 
      <label>
        <input
          type="checkbox"
          checked={showB}
          onChange={e => {
            setShowB(e.target.checked)
          }}
        />
        Рендерити другий лічильник
      </label>
    </div>
  );
}

function Counter() {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Додати один
      </button>
    </div>
  );
}

Зверніть увагу, що як тільки ви зупиняєте рендеринг другого лічильника, його стан повністю зникає. Це тому, що коли React видаляє компонент, він знищує його стан.

Діаграма дерева React-компонентів. Кореневий вузол позначений як 'div' та має двоє дочірніх компонентів. Лівий нащадок має назву 'Counter' та містить бульбашку стану з назвою 'count' із значенням 0. Правий нащадок відсутній, проте на його місці знаходиться жовта піктограма 'пуф', яка виділяє компонент, що видалився із дерева.
Діаграма дерева React-компонентів. Кореневий вузол позначений як 'div' та має двоє дочірніх компонентів. Лівий нащадок має назву 'Counter' та містить бульбашку стану з назвою 'count' із значенням 0. Правий нащадок відсутній, проте на його місці знаходиться жовта піктограма 'пуф', яка виділяє компонент, що видалився із дерева.

Видалення компонента

Коли ви вибираєте “Рендерити другий лічильник”, другий Counter і його стан ініціалізуються з нуля (рахунок = 0) і додаються до DOM.

Діаграма дерева React-компонентів. Кореневий вузол позначений як 'div' та має двоє дочірніх компонентів. Лівий нащадок має назву 'Counter' та містить бульбашку стану з назвою 'count' із значенням 0. Правий нащадок має назву 'Counter' та містить бульбашку стану з назвою 'count' із значенням 0. Весь правий дочірній вузол виділено жовтим, що вказує на те, що він був добавлений до дерева.
Діаграма дерева React-компонентів. Кореневий вузол позначений як 'div' та має двоє дочірніх компонентів. Лівий нащадок має назву 'Counter' та містить бульбашку стану з назвою 'count' із значенням 0. Правий нащадок має назву 'Counter' та містить бульбашку стану з назвою 'count' із значенням 0. Весь правий дочірній вузол виділено жовтим, що вказує на те, що він був добавлений до дерева.

Додавання компонента

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

Той самий компонент у тій самій позиції зберігає стан

У цьому прикладі є два різних <Counter /> теги:

import { useState } from 'react';

export default function App() {
  const [isFancy, setIsFancy] = useState(false);
  return (
    <div>
      {isFancy ? (
        <Counter isFancy={true} /> 
      ) : (
        <Counter isFancy={false} /> 
      )}
      <label>
        <input
          type="checkbox"
          checked={isFancy}
          onChange={e => {
            setIsFancy(e.target.checked)
          }}
        />
        Використати вишукану стилізацію
      </label>
    </div>
  );
}

function Counter({ isFancy }) {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }
  if (isFancy) {
    className += ' fancy';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Додати один
      </button>
    </div>
  );
}

Коли ви встановлюєте або знімаєте прапорець, стан лічильника не обнуляється. Не залежно від того, чи isFancy є true або false, у вас завжди є <Counter /> як перший дочірній елемент div, що повертається з кореневого компонента App.

Діаграма з двома секціями, розділеними стрілкою, що переходить між ними. Кожна секція містить макет компонентів з батьківським компонентом, позначеним як 'App', що містить бульбашку стану, позначену як isFancy. Цей компонент має одного дочірнього елемента з назвою 'div', що створює бульбашку пропсів, яка містить isFancy (виділено фіолетовим кольором), що передається єдиному дочірньому елементу. Останній нащадок, що має назву 'Counter' і містить бульбашку станку з назвою 'count' та значенням 3 на обох діаграмах. У лівій частині діаграми нічого не виділено, а значення батьківського isFancy дорівнює false. У правій частині діаграми, значення батьківського стану isFancy змінилося на true і він виділений жовтим, так само як і бульбашка пропсів нижче, яка також змінила своє значення isFancy на true.
Діаграма з двома секціями, розділеними стрілкою, що переходить між ними. Кожна секція містить макет компонентів з батьківським компонентом, позначеним як 'App', що містить бульбашку стану, позначену як isFancy. Цей компонент має одного дочірнього елемента з назвою 'div', що створює бульбашку пропсів, яка містить isFancy (виділено фіолетовим кольором), що передається єдиному дочірньому елементу. Останній нащадок, що має назву 'Counter' і містить бульбашку станку з назвою 'count' та значенням 3 на обох діаграмах. У лівій частині діаграми нічого не виділено, а значення батьківського isFancy дорівнює false. У правій частині діаграми, значення батьківського стану isFancy змінилося на true і він виділений жовтим, так само як і бульбашка пропсів нижче, яка також змінила своє значення isFancy на true.

Оновлення стану App не обнуляє Counter, оскільки Counter залишається на тій самій позиції

Це той самий компонент на тій самій позиції, отже з точки зору React, це той самий лічильник.

Будьте обачні

Пам’ятайте, що це позиція в дереві інтерфейсу користувача — не в JSX-розмітці — для React це важливо! Цей компонент має два return пункти з різними <Counter /> JSX-тегами в середині та зовні if:

import { useState } from 'react';

export default function App() {
  const [isFancy, setIsFancy] = useState(false);
  if (isFancy) {
    return (
      <div>
        <Counter isFancy={true} />
        <label>
          <input
            type="checkbox"
            checked={isFancy}
            onChange={e => {
              setIsFancy(e.target.checked)
            }}
          />
          Використати вишукану стилізацію
        </label>
      </div>
    );
  }
  return (
    <div>
      <Counter isFancy={false} />
      <label>
        <input
          type="checkbox"
          checked={isFancy}
          onChange={e => {
            setIsFancy(e.target.checked)
          }}
        />
        Використати вишукану стилізацію
      </label>
    </div>
  );
}

function Counter({ isFancy }) {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }
  if (isFancy) {
    className += ' fancy';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Додати один
      </button>
    </div>
  );
}

Ви можете очікувати, що стан буде обнулено, коли ви поставите галочку, але цього не станеться! Це тому, що обидва теги <Counter /> рендеряться на тій самій позиції. React не знає, де ви розміщуєте умови у вашій функції. Все, що він “бачить” — це дерево, що ви повертаєте.

В обох випадках, App компонент повертає <div> із <Counter /> як першим дочірнім елементом. Для React ці два лічильника мають однакову “адресу”: перший нащадок першого нащадка кореня. Ось як React зіставляє їх між попереднім і наступним рендерингом, незалежно від того, як ви структуруєте свою логіку.

Різні компоненти на тій самій позиції скидають стан

В цьому прикладі встановлення прапорця замінить <Counter> на <p>:

import { useState } from 'react';

export default function App() {
  const [isPaused, setIsPaused] = useState(false);
  return (
    <div>
      {isPaused ? (
        <p>Побачимось пізніше!</p> 
      ) : (
        <Counter /> 
      )}
      <label>
        <input
          type="checkbox"
          checked={isPaused}
          onChange={e => {
            setIsPaused(e.target.checked)
          }}
        />
        Зробіть перерву
      </label>
    </div>
  );
}

function Counter() {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Додати один
      </button>
    </div>
  );
}

Тут, ви переключаєте між різними типами компонент на одній позиції. Початково перший дочірній елемент <div> містив Counter. Але коли ви замінили його на p, React видалив Counter з дерева інтерфейсу та знищив його стан.

Діаграма з трьома секціями, розділеними стрілкою, що переходить між ними. Перша секція містить React компонент названий 'div' з єдиним дочірнім елементом названим 'Counter', що містить бульбашковий стан названий 'count' із значенням 3. Центральна секція має той самий батьківський 'div' але дочірній компонент тепер видалено, на що вказує жовте зображення 'poof'. Третя секція має знову той самий батьківський 'div', тепер із новим дочірнім елементом названим 'p', який виділено жовтим.
Діаграма з трьома секціями, розділеними стрілкою, що переходить між ними. Перша секція містить React компонент названий 'div' з єдиним дочірнім елементом названим 'Counter', що містить бульбашковий стан названий 'count' із значенням 3. Центральна секція має той самий батьківський 'div' але дочірній компонент тепер видалено, на що вказує жовте зображення 'poof'. Третя секція має знову той самий батьківський 'div', тепер із новим дочірнім елементом названим 'p', який виділено жовтим.

Коли Counter змінюється на p, то Counter видалено та p добавлено

Діаграма з трьома секціями, розділеними стрілкою, що переходить між ними. Перша секція містить React компонент названий 'p'. Центральна секція має той самий батьківський 'div' але дочірній компонент тепер видалено, на що вказує жовте зображення 'poof'. Третя секція має знову той самий батьківський 'div', тепер із новим дочірнім елементом названим 'Counter', що містить бульбашковий стан названий 'count' із значенням 0, що підсвічено жовтим.
Діаграма з трьома секціями, розділеними стрілкою, що переходить між ними. Перша секція містить React компонент названий 'p'. Центральна секція має той самий батьківський 'div' але дочірній компонент тепер видалено, на що вказує жовте зображення 'poof'. Третя секція має знову той самий батьківський 'div', тепер із новим дочірнім елементом названим 'Counter', що містить бульбашковий стан названий 'count' із значенням 0, що підсвічено жовтим.

Коли перемикаємо назад, p видалено та Counter додано

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

import { useState } from 'react';

export default function App() {
  const [isFancy, setIsFancy] = useState(false);
  return (
    <div>
      {isFancy ? (
        <div>
          <Counter isFancy={true} /> 
        </div>
      ) : (
        <section>
          <Counter isFancy={false} />
        </section>
      )}
      <label>
        <input
          type="checkbox"
          checked={isFancy}
          onChange={e => {
            setIsFancy(e.target.checked)
          }}
        />
        Використати вишукану стилізацію
      </label>
    </div>
  );
}

function Counter({ isFancy }) {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }
  if (isFancy) {
    className += ' fancy';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Додати один
      </button>
    </div>
  );
}

Стан лічильника скидається коли ви натискаєте на прапорець. Хоча ви рендерите Counter, перший дочірній елемент div змінюється з div на section. Коли дочірній елемент div було видалено з DOM, все дерево під ним (включаючи Counter та його стан) було також знищено.

Діаграма з трьома секціями, розділеними стрілкою, що переходить між ними. Перша секція містить React компонент названий 'div' з єдиним дочірнім елементом названим 'section', який має єдиний дочірній елемент названий 'Counter', що містить бульбашковий стан названий 'count' із значенням 3. Центральна секція має той самий батьківський 'div' але дочірні компоненти тепер видалені, на що вказує жовте зображення 'poof'. Третя секція має знову той самий батьківський 'div', тепер із новим дочірнім елементом названим 'div', що підсвічено жовтим, також із новим дочірнім елементом названим 'Counter' що містить бульбашковий стан названий 'count' із значенням 0, все підсвічено жовтим.
Діаграма з трьома секціями, розділеними стрілкою, що переходить між ними. Перша секція містить React компонент названий 'div' з єдиним дочірнім елементом названим 'section', який має єдиний дочірній елемент названий 'Counter', що містить бульбашковий стан названий 'count' із значенням 3. Центральна секція має той самий батьківський 'div' але дочірні компоненти тепер видалені, на що вказує жовте зображення 'poof'. Третя секція має знову той самий батьківський 'div', тепер із новим дочірнім елементом названим 'div', що підсвічено жовтим, також із новим дочірнім елементом названим 'Counter' що містить бульбашковий стан названий 'count' із значенням 0, все підсвічено жовтим.

Коли section змінюється на div, тоді section видаляється і новий div додається

Діаграма з трьома секціями, розділеними стрілкою, що переходить між ними. Перша секція містить React компонент названий 'div' з єдиним дочірнім елементом названим 'div', який має єдиний дочірній елемент названий 'Counter', що містить бульбашковий стан названий 'count' із значенням 0. Центральна секція має той самий батьківський 'div' але дочірні компоненти тепер видалені, на що вказує жовте зображення 'poof'. Третя секція має знову той самий батьківський 'div', тепер із новим дочірнім елементом названим 'section', що підсвічено жовтим, також із новим дочірнім елементом названим 'Counter' що містить бульбашковий стан названий 'count' із значенням 0, все підсвічено жовтим.
Діаграма з трьома секціями, розділеними стрілкою, що переходить між ними. Перша секція містить React компонент названий 'div' з єдиним дочірнім елементом названим 'div', який має єдиний дочірній елемент названий 'Counter', що містить бульбашковий стан названий 'count' із значенням 0. Центральна секція має той самий батьківський 'div' але дочірні компоненти тепер видалені, на що вказує жовте зображення 'poof'. Третя секція має знову той самий батьківський 'div', тепер із новим дочірнім елементом названим 'section', що підсвічено жовтим, також із новим дочірнім елементом названим 'Counter' що містить бульбашковий стан названий 'count' із значенням 0, все підсвічено жовтим.

При перемиканні назад, div видаляється і нова section додається

Як правила, якщо ви хочете зберегти стан між повторними рендерами, структура вашого дерева повинна “збігатися” від одного рендера до іншого. Якщо структура відрізняється, стан знищується, оскільки React знищує стан, коли видаляє компонент із дерева.

Будьте обачні

Ось чому вам не варто вкладати визначення функційних компонент.

Тут, MyTextField функційний компонент визначено всередині MyComponent:

import { useState } from 'react';

export default function MyComponent() {
  const [counter, setCounter] = useState(0);

  function MyTextField() {
    const [text, setText] = useState('');

    return (
      <input
        value={text}
        onChange={e => setText(e.target.value)}
      />
    );
  }

  return (
    <>
      <MyTextField />
      <button onClick={() => {
        setCounter(counter + 1)
      }}>Натиснуто {counter} разів</button>
    </>
  );
}

Кожного разу, коли ви натискаєте кнопку, стан введення зникає! Це відбувається тому, що інша MyTextField функція створюється на кожен рендер MyComponent. Ви рендерите інший компонент у тому самому місці, тому React скидає весь стан нижче. Це призводить до проблем та проблем із продуктивністю. Щоб уникнути цієї проблеми завжди оголошуйте функційні компоненти на найвищому рівні і не вкладайте їхнє визначення.

Скидання стану в тому самому місці

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

import { useState } from 'react';

export default function Scoreboard() {
  const [isPlayerA, setIsPlayerA] = useState(true);
  return (
    <div>
      {isPlayerA ? (
        <Counter person="Тайлера" />
      ) : (
        <Counter person="Сари" />
      )}
      <button onClick={() => {
        setIsPlayerA(!isPlayerA);
      }}>
        Наступний гравець!
      </button>
    </div>
  );
}

function Counter({ person }) {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>Рахунок {person}: {score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Додати один
      </button>
    </div>
  );
}

Зараз, коли ви змінюєте гравця, рахунок зберігається. Обидва Counter з’являються на одній і тій самій позиції, тому React сприймає їх як один і той самий Counter, у якого просто змінився проп person.

Але концептуально в цьому застосунку вони повинні бути двома окремими лічильниками. Вони можуть відображатися на одному й тому ж місці в інтерфейсі користувача, але один з них є лічильником для Тейлора, а інший — лічильником для Сари.

Є два способи скинути стан при перемиканні між ними:

  1. Рендерити компоненти на різних позиціях
  2. Дати кожному компоненту явну ідентичність за допомогою key

Спосіб 1: Рендерити компонент в різних місцях

Якщо ви хочете, щоб ці два Counter, були незалежними, вам потрібно рендерити їх в двох різних позиціях:

import { useState } from 'react';

export default function Scoreboard() {
  const [isPlayerA, setIsPlayerA] = useState(true);
  return (
    <div>
      {isPlayerA &&
        <Counter person="Тайлера" />
      }
      {!isPlayerA &&
        <Counter person="Сари" />
      }
      <button onClick={() => {
        setIsPlayerA(!isPlayerA);
      }}>
        Наступний гравець!
      </button>
    </div>
  );
}

function Counter({ person }) {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>Рахунок {person}: {score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Додати один
      </button>
    </div>
  );
}

  • Спочатку isPlayerA дорівнює true. Тому перша позиція містить стан Counter, а друга — порожня.
  • Коли ви натискаєте кнопку “Наступний гравець”, перша позиція очищується, а друга тепер містить Counter.
Діаграма з деревом React компонент. Батько названий 'Scoreboard' із бульбашковим станом названим isPlayerA із значенням 'true'. Єдиний дочірній елемент, розташований ліворуч, названий Counter із бульбашковим станом названим 'count' із значенням 0. Вся ліва частина дочірніх елементів підсвічена жовтим, що вказує на те, що вона була додана.
Діаграма з деревом React компонент. Батько названий 'Scoreboard' із бульбашковим станом названим isPlayerA із значенням 'true'. Єдиний дочірній елемент, розташований ліворуч, названий Counter із бульбашковим станом названим 'count' із значенням 0. Вся ліва частина дочірніх елементів підсвічена жовтим, що вказує на те, що вона була додана.

Початковий стан

Діаграма з деревом React компонент. Батько названий 'Scoreboard' із бульбашковим станом названим isPlayerA із значенням 'false'. Бульбашковий стан підсвічено жовтим, що вказує, на його зміну. Лівий дочірній елемент змінено на жовте 'poof' зображення, що вказує на те, що воно було видалено, а праворуч появився новий дочірній елемент, що підсвічено жовтим, що вказує на те, що його було додано. Новий дочірній елемент названо 'Counter' та містить бульбашковий стан названий 'count' із значенням 0.
Діаграма з деревом React компонент. Батько названий 'Scoreboard' із бульбашковим станом названим isPlayerA із значенням 'false'. Бульбашковий стан підсвічено жовтим, що вказує, на його зміну. Лівий дочірній елемент змінено на жовте 'poof' зображення, що вказує на те, що воно було видалено, а праворуч появився новий дочірній елемент, що підсвічено жовтим, що вказує на те, що його було додано. Новий дочірній елемент названо 'Counter' та містить бульбашковий стан названий 'count' із значенням 0.

Натискання “наступний”

Діаграма з деревом React компонент. Батько названий 'Scoreboard' із бульбашковим станом названим isPlayerA із значенням 'true'. Бульбашковий стан підсвічено жовтим, що вказує, на його зміну. Ліворуч появився новий дочірній елемент, який підсвічено жовтим, що вказує на те, що його було додано. Новий дочірній елемент названий 'Counter' та містить бульбашковий стан названий 'count' із значенням 0. Правий дочірній елемента замінено на 'poof' зображення, що вказує на те, що його було видалено.
Діаграма з деревом React компонент. Батько названий 'Scoreboard' із бульбашковим станом названим isPlayerA із значенням 'true'. Бульбашковий стан підсвічено жовтим, що вказує, на його зміну. Ліворуч появився новий дочірній елемент, який підсвічено жовтим, що вказує на те, що його було додано. Новий дочірній елемент названий 'Counter' та містить бульбашковий стан названий 'count' із значенням 0. Правий дочірній елемента замінено на 'poof' зображення, що вказує на те, що його було видалено.

Натискання “наступний” знову

Стан кожного Counter знищується щоразу, коли його видаляють із DOM. Саме тому вони скидаються щоразу, коли ви натискаєте кнопку.

Це рішення зручне, коли у вас лише кілька незалежних компонент, які рендеряться на одному місці. У цьому прикладі їх лише два, тому не складно рендерити обидві окремо в JSX.

Спосіб 2: Скидання стану за допомогою ключа

Існує також інший, більш загальний спосіб скидання стану компонента.

Ви могли бачити key коли рендерите списки. Ключі не тільки для списків! Ви можете використовувати ключі, щоб React розрізняв будь-які компоненти. За замовчуванням React використовує порядок в середині батьківського елемента (“перший лічильник”, “другий лічильник”), щоб розрізняти компоненти. Але ключі дають можливість вам сказати до React, що це не просто перший лічильник або другий лічильник а конкретний лічильник — для прикладу, лічильник Тайлера. Таким чином, React буде знати лічильник Тайлера будь-коли він з’явиться в дереві!

В цьому прикладі, два <Counter /> не поширюють стан, хоча вони появляються в одному місці в JSX:

import { useState } from 'react';

export default function Scoreboard() {
  const [isPlayerA, setIsPlayerA] = useState(true);
  return (
    <div>
      {isPlayerA ? (
        <Counter key="Taylor" person="Тайлера" />
      ) : (
        <Counter key="Sarah" person="Сари" />
      )}
      <button onClick={() => {
        setIsPlayerA(!isPlayerA);
      }}>
        Наступний гравець!
      </button>
    </div>
  );
}

function Counter({ person }) {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>Рахунок {person}: {score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Додати один
      </button>
    </div>
  );
}

Переключення між станом Тайлера і Сари не зберігає стан. Це тому що ви дали їм різні key:

{isPlayerA ? (
<Counter key="Taylor" person="Тайлер" />
) : (
<Counter key="Sarah" person="Сара" />
)}

Вказуючи key ви кажете React використовувати key як частину місця розташування замість його власного порядку в середині батьківського елемента. Саме через це, навіть якщо ви рендерите їх в одному місці в JSX, React бачить їх як два різних лічильника, саме тому вони ніколи не поширюватимуть стан. Кожного разу коли лічильник появляється на екрані, його стан створюється. Кожен раз коли він видалений, його стан знищується. Переключення між ними скидає їхній стан знову і знову.

Примітка

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

Скидання форми за допомогою ключа

Скидання стану за допомогою ключа є особливо корисне при роботі з формами.

В цьому чат-застосунку компонент <Chat> містить стан поля введення тексту:

import { useState } from 'react';
import Chat from './Chat.js';
import ContactList from './ContactList.js';

export default function Messenger() {
  const [to, setTo] = useState(contacts[0]);
  return (
    <div>
      <ContactList
        contacts={contacts}
        selectedContact={to}
        onSelect={contact => setTo(contact)}
      />
      <Chat contact={to} />
    </div>
  )
}

const contacts = [
  { id: 0, name: 'Тайлер', email: 'taylor@mail.com' },
  { id: 1, name: 'Аліса', email: 'alice@mail.com' },
  { id: 2, name: 'Боб', email: 'bob@mail.com' }
];

Спробуйте ввести щось в поле введення і потім натиснути “Аліса” або “Боб” щоб вибрати іншого отримувача. Ви помітите, що стан поля введення збережено, тому що <Chat> рендериться в тому самому місці в дереві.

В більшості застосунків це може бути бажана поведінка але не в чат-застосунку! Ви не хочете дозволити користувачу надіслати повідомлення, яке вони вже написали до іншої особи через випадкове натискання. Щоб виправити це, додайте key:

<Chat key={to.id} contact={to} />

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

Тепер при перемиканні отримувача поле введення завжди очищується:

import { useState } from 'react';
import Chat from './Chat.js';
import ContactList from './ContactList.js';

export default function Messenger() {
  const [to, setTo] = useState(contacts[0]);
  return (
    <div>
      <ContactList
        contacts={contacts}
        selectedContact={to}
        onSelect={contact => setTo(contact)}
      />
      <Chat key={to.id} contact={to} />
    </div>
  )
}

const contacts = [
  { id: 0, name: 'Тайлер', email: 'taylor@mail.com' },
  { id: 1, name: 'Аліса', email: 'alice@mail.com' },
  { id: 2, name: 'Боб', email: 'bob@mail.com' }
];

Занурення

Збереження стану для видалених компонент

В справжньому чат-застосунку, ви можливо захочете відновити стан поля введення коли користувач знову вибирає попереднього отримувача. Є декілька способів, щоб зберегти стан “живим” для компоненти, що більше не є видимою:

  • Ви можете рендерити всі чати замість одного поточного але сховати всі інші за допомогою CSS. Чати не будуть видалені із дерева, тому їхній локальний стан буде збережено. Це рішення добре працює для простих інтерфейсів користувача. Але, воно може стати дуже повільним, якщо приховані дерева великі та містять багато DOM-вузлів.
  • Ви можете підняти стан вгору і тримати очікуване повідомлення для кожного отримувача в батьківському компоненті. Цим методом, коли дочірній компонент буде видалено, це немає значення, тому що його батько зберігає важливу інформацію. Це найпоширеніше рішення.
  • Ви також можете використовувати інший джерело разом із станом React. Для прикладу, ви можливо хочете, щоб чернетка із повідомленням була присутня навіть коли користувач випадково закрив сторінку. Щоб реалізувати це, вам потрібно щоб Chat компонент ініціалізував власний стан і читав його із локального сховища, та зберігав чернетку там.

Не залежно від того, яку стратегію ви оберете, чат із Алісою концептуально відрізняється від чату з Бобом, тому є сенс надати ключ до <Chat> дерева на основі поточного отримувача.

Підсумок

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

Завдання 1 із 5:
Виправити зникнення введеного тексту

В цьому прикладі показано повідомлення коли ви натискаєте на кнопку. Однак, натискання кнопки також випадково очищає поле введення. Чому це трапляється? Виправте це, щоб під час натискання кнопки поле для введення не очищувалось.

import { useState } from 'react';

export default function App() {
  const [showHint, setShowHint] = useState(false);
  if (showHint) {
    return (
      <div>
        <p><i>Підказка: Яке ваше улюблене місто?</i></p>
        <Form />
        <button onClick={() => {
          setShowHint(false);
        }}>Сховати підказку</button>
      </div>
    );
  }
  return (
    <div>
      <Form />
      <button onClick={() => {
        setShowHint(true);
      }}>Показати підказку</button>
    </div>
  );
}

function Form() {
  const [text, setText] = useState('');
  return (
    <textarea
      value={text}
      onChange={e => setText(e.target.value)}
    />
  );
}