Сохранение и сброс состояния
Состояние изолировано между компонентами. React отслеживает, какое состояние принадлежит какому компоненту, основываясь на их месте в дереве пользовательского интерфейса. Вы можете контролировать, когда сохранять состояние и когда сбрасывать его между повторными рендерингами.
You will learn
- Как React «видит» структуру компонентов
- Когда React выбирает сохранение или сброс состояния
- Как заставить React сбросить состояние компонента
- Как ключи и типы влияют на сохранение состояния
Дерево UI
Браузеры используют множество древовидных структур для моделирования пользовательского UI. The DOM представляет элементы HTML, CSSOM делает тоже самое для CSS. Есть даже Дерево специальных возможностей!
React также использует древовидные структуры для управления и моделирования пользовательского интерфейса, который вы создаете. React создает UI деревья из вашего JSX. Затем React DOM обновляет элементы DOM браузера, чтобы они соответствовали этому дереву пользовательского интерфейса. (React Native переводит эти деревья в элементы, специфичные для мобильных платформ.)
Состояние привязано к положению в дереве
Когда вы задаете состояние компонента, вы можете подумать, что это состояние «живет» внутри компонента. Но на самом деле состояние хранится внутри React. React связывает каждую часть состояния, которую он хранит, с правильным компонентом в зависимости от того, где этот компонент находится в дереве UI.
Здесь есть только один <Counter />
тег JSX, но он отображается в двух разных позициях:
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)}> Add one </button> </div> ); }
Вот как они выглядят в виде дерева:
Это два отдельных счетчика, поскольку каждый из них отображается в своей позиции в дереве. Обычно вам не нужно думать об этих позициях, чтобы использовать 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)}> Add one </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) }} /> Render the second counter </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)}> Add one </button> </div> ); }
Обратите внимание, как в тот момент, когда вы прекращаете рендеринг второго счетчика, его состояние полностью исчезает. Это потому, что когда React удаляет компонент, он уничтожает его состояние.
Когда вы ставите галочку “рендерить второй счетчик”, второй Counter
и ее состояние инициализируются с нуля (score = 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) }} /> Use fancy styling </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)}> Add one </button> </div> ); }
При установке или снятии флажка состояние счетчика не сбрасывается. Независимо от того, isFancy
является true
или false
, у вас всегда есть <Counter />
как первый дочерний элемент, возвращенный div
returned из корневого App
компонента:
Это тот же компонент в той же позиции, поэтому с точки зрения React это тот же counter.
Состояние сброса разных компонентов в одном и том же положении
В этом примере установка флага заменит <Counter>
на <p>
:
import { useState } from 'react'; export default function App() { const [isPaused, setIsPaused] = useState(false); return ( <div> {isPaused ? ( <p>See you later!</p> ) : ( <Counter /> )} <label> <input type="checkbox" checked={isPaused} onChange={e => { setIsPaused(e.target.checked) }} /> Take a break </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)}> Add one </button> </div> ); }
Здесь вы переключаетесь между различными типами компонентов в одной и той же позиции. Первоначально первый дочерний элемент <div>
содержал Counter
. Но когда вы заменили p
, React удалил Counter
из дерева UI и уничтожил его состояние.
Кроме того, когда вы визуализируете другой компонент в той же позиции, он сбрасывает состояние всего его поддерева. Чтобы увидеть, как это работает, увеличьте счетчик и установите флажок:
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) }} /> Use fancy styling </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)}> Add one </button> </div> ); }
Состояние счетчика сбрасывается при установке флажка. Хотя вы рендерите Counter
, первый дочерний элемент div
изменяется с div
на section
. Когда дочерний элемент div
был удален из DOM, все дерево под ним (включая Counter
и его состояние) также было уничтожено.
Как правило, если вы хотите сохранить состояние между повторными рендерингами, структура вашего дерева должна «сопоставляться от одного рендеринга к другому. Если структура отличается, состояние уничтожается, потому что React уничтожает состояние, когда удаляет компонент из дерева.
Сброс состояния в той же позиции
По умолчанию React сохраняет состояние компонента, пока он остается в том же положении. Обычно это именно то, что вам нужно, поэтому это имеет смысл в качестве поведения по умолчанию. Но иногда вам может понадобиться сбросить состояние компонента. Рассмотрим это приложение, которое позволяет двум игрокам отслеживать свои очки во время каждого хода:
import { useState } from 'react'; export default function Scoreboard() { const [isPlayerA, setIsPlayerA] = useState(true); return ( <div> {isPlayerA ? ( <Counter person="Taylor" /> ) : ( <Counter person="Sarah" /> )} <button onClick={() => { setIsPlayerA(!isPlayerA); }}> Next player! </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}'s score: {score}</h1> <button onClick={() => setScore(score + 1)}> Add one </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="Taylor" /> } {!isPlayerA && <Counter person="Sarah" /> } <button onClick={() => { setIsPlayerA(!isPlayerA); }}> Next player! </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}'s score: {score}</h1> <button onClick={() => setScore(score + 1)}> Add one </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="Taylor" /> ) : ( <Counter key="Sarah" person="Sarah" /> )} <button onClick={() => { setIsPlayerA(!isPlayerA); }}> Next player! </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}'s score: {score}</h1> <button onClick={() => setScore(score + 1)}> Add one </button> </div> ); }
Переключение между Тейлором и Сарой не сохраняет состояние. Это потому, что вы дали им разные key
s:
{isPlayerA ? (
<Counter key="Taylor" person="Taylor" />
) : (
<Counter key="Sarah" person="Sarah" />
)}
Указание 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: 'Taylor', email: 'taylor@mail.com' }, { id: 1, name: 'Alice', email: 'alice@mail.com' }, { id: 2, name: 'Bob', 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: 'Taylor', email: 'taylor@mail.com' }, { id: 1, name: 'Alice', email: 'alice@mail.com' }, { id: 2, name: 'Bob', email: 'bob@mail.com' } ];
Deep Dive
В реальном приложении чата вы, вероятно, захотите восстановить состояние ввода, когда пользователь снова выбирает предыдущего получателя. Есть несколько способов сохранить состояние «живым» для компонента, который больше не виден:
- Вы можете отобразить все чаты, а не только текущий, но скрыть все остальные с помощью CSS. Чаты не будут удалены из дерева, поэтому их локальное состояние будет сохранено. Это решение отлично работает для простых пользовательских интерфейсов. Но это может быть очень медленным, если скрытые деревья большие и содержат много узлов DOM.
- Вы можете поднять состояние и сохранить ожидающее сообщение для каждого получателя в родительском компоненте. Таким образом, когда удаляются дочерние компоненты, это не имеет значения, потому что родительский компонент сохраняет важную информацию. Это наиболее распространенное решение.
- Вы также можете использовать другой источник в дополнение к состоянию React. Например, вы, вероятно, хотите, чтобы черновик сообщения сохранялся, даже если пользователь случайно закроет страницу. Чтобы реализовать это, вы можете сделать так, чтобы
Chat
компонент инициализировал свое состояние, читая из файлаlocalStorage
, и сохранял там черновики.
Независимо от того, какую стратегию вы выберете, чат с Алисой концептуально отличается от чата с Бобом, поэтому имеет смысл задать key
<Chat>
на основе текущего получателя.
Recap
- React сохраняет состояние до тех пор, пока один и тот же компонент отображается в одной и той же позиции.
- Состояние не сохраняется в тегах JSX. Он связан с позицией дерева, в которую вы помещаете этот JSX.
- Вы можете заставить поддерево сбросить свое состояние, назначив ему другой ключ.
- Не вкладывайте определения компонентов, иначе вы случайно сбросите состояние.
Challenge 1 of 5: Исправление исчезающего ввода текста
В этом примере показано сообщение при нажатии кнопки. Однако нажатие кнопки также случайно сбрасывает ввод. Почему это происходит? Исправьте, чтобы нажатие на кнопку не сбрасывало вводимый текст.
import { useState } from 'react'; export default function App() { const [showHint, setShowHint] = useState(false); if (showHint) { return ( <div> <p><i>Hint: Your favorite city?</i></p> <Form /> <button onClick={() => { setShowHint(false); }}>Hide hint</button> </div> ); } return ( <div> <Form /> <button onClick={() => { setShowHint(true); }}>Show hint</button> </div> ); } function Form() { const [text, setText] = useState(''); return ( <textarea value={text} onChange={e => setText(e.target.value)} /> ); }