Сохранение и сброс состояния
Состояние изолировано между компонентами. React отслеживает, какое состояние принадлежит какому компоненту, исходя из их расположения в дереве пользовательского интерфейса. Вы можете управлять тем, когда сохранять состояние, а когда сбрасывать его между перерисовками.
Вы узнаете
- Когда React выбирает сохранять или сбрасывать состояние
- Как заставить React сбросить состояние компонента
- Как ключи и типы влияют на сохранение состояния
Состояние привязано к позиции в дереве рендеринга
React строит деревья рендеринга для структуры компонентов в вашем пользовательском интерфейсе.
Когда вы предоставляете компоненту состояние, вы можете подумать, что состояние «живет» внутри компонента. Но состояние фактически хранится внутри React. React связывает каждое хранимое им состояние с соответствующим компонентом в зависимости от того, где этот компонент находится в дереве рендеринга.
Здесь есть только один <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, но это может быть полезно, чтобы понять, как он работает.
В 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 возвращаемого корневым App компонентом:
Обновление состояния App не сбрасывает состояние Counter, потому что Counter остаётся в той же позиции
Это тот же компонент в той же позиции, поэтому с точки зрения React это тот же счётчик.
Опасная ситуация
Помните, что для React важна позиция в дереве пользовательского интерфейса, а не в разметке JSX! Этот компонент имеет два 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) }} /> Use fancy styling </label> </div> ); } return ( <div> <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> ); }
Вы можете ожидать, что состояние сбросится при установке флажка, но этого не происходит! Это происходит потому, что оба эти <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>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 из дерева пользовательского интерфейса и уничтожил его состояние.
При изменении 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) }} /> 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 и его состояние) также было уничтожено.
При изменении section на div, section удаляется, и новый div добавляется
При возвращении к предыдущему, 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) }}>Clicked {counter} times</button> </> ); }
Каждый раз, когда вы нажимаете кнопку, состояние ввода исчезает! Это происходит потому, что для каждой перерисовки MyComponent создаётся новая функция компонента MyTextField. Вы отображаете другой компонент в одной и той же позиции, поэтому 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> ); }
- Изначально,
isPlayerAtrue. Таким образом, первая позиция содержит состояние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> ); }
Переключение между Тейлором и Сарой не сохраняет состояние. Это происходит потому, что вы присвоили им разные keys:
{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' } ];
Подробный анализ
Сохранение состояния для удаленных компонентов
В реальном приложении чата, вероятно, вы захотите восстановить состояние поля ввода, когда пользователь снова выберет предыдущего получателя. Есть несколько способов сохранить состояние «живым» для компонента, который больше не отображается:
- Вы можете отобразить все чаты, а не только текущий, но скрыть все остальные с помощью CSS. Чаты не будут удалены из дерева, поэтому их локальное состояние будет сохранено. Это решение отлично подходит для простых пользовательских интерфейсов. Но оно может стать очень медленным, если скрытые деревья большие и содержат много узлов DOM.
- Вы можете поднять состояние наверх и хранить ожидаемое сообщение для каждого получателя в родительском компоненте. Таким образом, когда дочерние компоненты удаляются, это не имеет значения, потому что родительский компонент сохраняет важную информацию. Это наиболее распространённое решение.
- Вы также можете использовать другой источник помимо состояния React. Например, вам, вероятно, нужно, чтобы черновик сообщения сохранялся даже если пользователь случайно закрывает страницу. Для реализации этого вы можете настроить компонент
Chatна инициализацию его состояния с помощью считывания изlocalStorage, и сохранять черновики там же.
Независимо от выбранной стратегии, чат с Элис концептуально отличается от чата с Бобом, поэтому имеет смысл присваивать key дереву <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>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)} /> ); }
© 2013–present Facebook Inc.
Licensed under the Creative Commons Attribution 4.0 International Public License.
https://react.dev/learn/preserving-and-resetting-state