Збереження та скидання стану
Стан ізольований між компонентами. 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 дерево
Це два окремі лічильника, оскільки кожен із них рендериться на своїй позиції в дереві. Зазвичай вам не потрібно думати про ці позиції щоб використовувати 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 зберігатиме цей стан доти, доки ви рендерите той самий компонент у тій самій позиції в дереві. Щоб побачити це, збільште обидва лічильника, потім видаліть другий компонент, знявши галочку “Рендерити другий лічильник”, а потім додайте його назад, поставивши галочку знову:
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 видаляє компонент, він знищує його стан.


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


Додавання компонента
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
не обнуляє Counter
, оскільки Counter
залишається на тій самій позиції
Це той самий компонент на тій самій позиції, отже з точки зору 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
з дерева інтерфейсу та знищив його стан.


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


Коли перемикаємо назад, 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
та його стан) було також знищено.


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


При перемиканні назад, div
видаляється і нова section
додається
Як правила, якщо ви хочете зберегти стан між повторними рендерами, структура вашого дерева повинна “збігатися” від одного рендера до іншого. Якщо структура відрізняється, стан знищується, оскільки 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
.
Але концептуально в цьому застосунку вони повинні бути двома окремими лічильниками. Вони можуть відображатися на одному й тому ж місці в інтерфейсі користувача, але один з них є лічильником для Тейлора, а інший — лічильником для Сари.
Є два способи скинути стан при перемиканні між ними:
- Рендерити компоненти на різних позиціях
- Дати кожному компоненту явну ідентичність за допомогою
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
.


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


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


Натискання “наступний” знову
Стан кожного 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)} /> ); }