Сохранение и сброс состояния
Состояние изолировано между компонентами. React отслеживает, какое состояние принадлежит какому компоненту, основываясь на их положении в дереве пользовательского интерфейса. Вы можете контролировать, когда сохранять состояние, а когда сбрасывать его между повторными рендерами.
Вы узнаете
- Когда React решает сохранить или сбросить состояние
- Как заставить React сбросить состояние компонента
- Как ключи и типы влияют на сохранение состояния
Состояние привязано к позиции в дереве рендеринга
React строитдеревья рендерингадля структуры компонентов в вашем пользовательском интерфейсе.
Когда вы добавляете компоненту состояние, вы можете думать, что состояние «живет» внутри компонента. Но на самом деле состояние хранится внутри React. React связывает каждый фрагмент состояния, который он хранит, с правильным компонентом, основываясь на том, где этот компонент находится в дереве рендеринга.
Здесь есть только один JSX-тег<Counter />, но он рендерится в двух разных позициях:
Вот как это выглядит в виде дерева:


Дерево React
Это два отдельных счетчика, потому что каждый рендерится на своей собственной позиции в дереве.Обычно вам не нужно думать об этих позициях при использовании React, но понимание того, как это работает, может быть полезным.
В React каждый компонент на экране имеет полностью изолированное состояние. Например, если вы рендерите два компонентаCounterрядом, каждый из них получит свои собственные, независимые состоянияscore и hover.
Попробуйте нажать на оба счетчика и обратите внимание, что они не влияют друг на друга:
Как видите, при обновлении одного счетчика обновляется только состояние этого компонента:


Обновление состояния
React будет сохранять состояние до тех пор, пока вы рендерите тот же компонент на той же позиции в дереве. Чтобы увидеть это, увеличьте оба счетчика, затем удалите второй компонент, сняв флажок «Рендерить второй счетчик», а затем снова добавьте его, установив флажок:
Обратите внимание, как в момент, когда вы перестаете рендерить второй счетчик, его состояние полностью исчезает. Это происходит потому, что когда React удаляет компонент, он уничтожает его состояние.


Удаление компонента
Когда вы отмечаете «Render the second counter», второйCounterи его состояние инициализируются с нуля (score = 0) и добавляются в DOM.


Добавление компонента
React сохраняет состояние компонента до тех пор, пока он отображается на своей позиции в дереве пользовательского интерфейса.Если он удаляется или на той же позиции отображается другой компонент, React отбрасывает его состояние.
Один и тот же компонент на одной и той же позиции сохраняет состояние
В этом примере есть два разных тега<Counter />:
Когда вы отмечаете или снимаете флажок, состояние счетчика не сбрасывается. Независимо от того,isFancyравноtrueилиfalse, у вас всегда есть<Counter />в качестве первого дочернего элементаdiv, возвращаемого из корневого компонентаApp:


Обновление состоянияAppне сбрасывает состояниеCounter, потому чтоCounterостается на той же позиции
Это один и тот же компонент на одной и той же позиции, поэтому с точки зрения React это один и тот же счетчик.
Подводный камень
Помните, чтодля React важна позиция в дереве пользовательского интерфейса, а не в JSX-разметке!Этот компонент имеет два предложенияreturnс разными тегами<Counter />внутри и вне условияif:
Вы могли ожидать, что состояние сбросится при установке флажка, но этого не происходит! Это потому, чтооба этих тега<Counter />отображаются на одной и той же позиции.React не знает, где вы размещаете условия в своей функции. Все, что он «видит», — это дерево, которое вы возвращаете.
В обоих случаях компонентAppвозвращает<div> с <Counter />в качестве первого дочернего элемента. Для React эти два счетчика имеют один и тот же «адрес»: первый дочерний элемент первого дочернего элемента корня. Именно так React сопоставляет их между предыдущим и следующим рендерами, независимо от того, как вы структурируете свою логику.
Разные компоненты на одной и той же позиции сбрасывают состояние
В этом примере установка флажка заменит<Counter> на <p>:
Здесь вы переключаетесь междуразнымитипами компонентов на одной и той же позиции. Изначально первый дочерний элемент<div>содержалCounter. Но когда вы подставилиp, React удалилCounterиз дерева пользовательского интерфейса и уничтожил его состояние.


КогдаCounterменяется наp, Counterудаляется, аpдобавляется


При обратном переключенииpудаляется, аCounterдобавляется
Кроме того,при рендеринге другого компонента на той же позиции сбрасывается состояние всего его поддерева.Чтобы увидеть, как это работает, увеличьте счётчик, а затем установите флажок:
Состояние счётчика сбрасывается при нажатии на флажок. Хотя вы рендеритеCounter, первый дочерний элементdivменяется сsectionнаdiv. Когда дочерний элементsectionбыл удалён из DOM, всё дерево под ним (включаяCounterи его состояние) также было уничтожено.


Когдаsectionменяется наdiv, sectionудаляется, а новыйdivдобавляется


При переключении обратноdivудаляется и добавляется новыйsection.
Как правило,если вы хотите сохранить состояние между повторными рендерами, структура вашего дерева должна «совпадать»от одного рендера к другому. Если структура отличается, состояние уничтожается, потому что React уничтожает состояние, когда удаляет компонент из дерева.
Подводный камень
Вот почему не следует вкладывать определения функций компонентов.
Здесь функция компонентаMyTextFieldопределенавнутриMyComponent:
Каждый раз при нажатии кнопки состояние ввода исчезает! Это происходит потому, что для каждого рендерадругаяMyTextField создаётся MyComponent. Вы рендеритедругойкомпонент в той же позиции, поэтому React сбрасывает всё состояние ниже. Это приводит к ошибкам и проблемам с производительностью. Чтобы избежать этой проблемы,всегда объявляйте функции компонентов на верхнем уровне и не вкладывайте их определения.
Сброс состояния в той же позиции
По умолчанию React сохраняет состояние компонента, пока он остаётся в той же позиции. Обычно это именно то, что вам нужно, поэтому такое поведение по умолчанию имеет смысл. Но иногда вы можете захотеть сбросить состояние компонента. Рассмотрим это приложение, которое позволяет двум игрокам отслеживать свои очки во время каждого хода:
В настоящее время при смене игрока счёт сохраняется. ДваCounterа отображаются в одной и той же позиции, поэтому React видит их какодин и тот жеCounter, у которого изменился пропсperson.
Но концептуально в этом приложении они должны быть двумя отдельными счётчиками. Они могут появляться в одном и том же месте в пользовательском интерфейсе, но один — это счётчик для Тейлора, а другой — счётчик для Сары.
Есть два способа сбросить состояние при переключении между ними:
- Рендерить компоненты в разных позициях
- Дать каждому компоненту явный идентификатор с помощью
key
Вариант 1: Рендеринг компонента в разных позициях
Если вы хотите, чтобы эти дваCounterа были независимыми, вы можете отрендерить их в двух разных позициях:
- Изначально
isPlayerAравноtrue. Поэтому первая позиция содержит состояниеCounter, а вторая пуста. - Когда вы нажимаете кнопку «Next player», первая позиция очищается, но теперь вторая содержит
Counter.


Начальное состояние


Нажатие «next»


Нажатие «next» снова
Состояние каждогоCounterуничтожается каждый раз, когда он удаляется из DOM. Вот почему они сбрасываются при каждом нажатии кнопки.
Это решение удобно, когда у вас есть всего несколько независимых компонентов, отображаемых в одном месте. В этом примере их всего два, поэтому нет проблем отображать их отдельно в JSX.
Вариант 2: Сброс состояния с помощью ключа
Существует также другой, более универсальный способ сброса состояния компонента.
Вы могли видетьkeyприотображении списков.Ключи предназначены не только для списков! Вы можете использовать ключи, чтобы React различал любые компоненты. По умолчанию React использует порядок внутри родителя («первый счётчик», «второй счётчик») для различения компонентов. Но ключи позволяют сообщить React, что это не простопервыйсчётчик иливторойсчётчик, а конкретный счётчик — например, счётчикТейлора. Таким образом, React будет знать счётчикТейлора, где бы он ни появлялся в дереве!
В этом примере два<Counter />не разделяют состояние, даже если они отображаются в одном месте в JSX:
Переключение между Тейлор и Сарой не сохраняет состояние. Это происходит потому, чтовы задали им разныеkey:
Указаниеkeyсообщает React использовать самkeyкак часть позиции, а не их порядок внутри родителя. Вот почему, даже если вы отображаете их в одном месте в JSX, React видит их как два разных счётчика, и поэтому они никогда не будут разделять состояние. Каждый раз, когда счётчик появляется на экране, его состояние создаётся. Каждый раз, когда он удаляется, его состояние уничтожается. Переключение между ними снова и снова сбрасывает их состояние.
Примечание
Помните, что ключи не являются глобально уникальными. Они указывают только позициювнутри родителя.
Сброс формы с помощью ключа
Сброс состояния с помощью ключа особенно полезен при работе с формами.
В этом чат-приложении компонент<Chat>содержит состояние текстового ввода:
Попробуйте ввести что-нибудь в поле ввода, а затем нажмите «Alice» или «Bob», чтобы выбрать другого получателя. Вы заметите, что состояние ввода сохраняется, потому что<Chat>отрисовывается на той же позиции в дереве.
Во многих приложениях такое поведение может быть желательным, но не в чате!Вы не хотите позволить пользователю отправить уже набранное сообщение не тому человеку из-за случайного клика. Чтобы это исправить, добавьтеkey:
Это гарантирует, что при выборе другого получателя компонентChatбудет создан заново с нуля, включая любое состояние в дереве под ним. React также пересоздаст DOM-элементы вместо их повторного использования.
Теперь переключение получателя всегда очищает текстовое поле:
Итоги
- React сохраняет состояние до тех пор, пока один и тот же компонент отрисовывается на той же позиции.
- Состояние не хранится в JSX-тегах. Оно связано с позицией в дереве, в которую вы поместили этот JSX.
- Вы можете принудительно сбросить состояние поддерева, задав ему другой ключ.
- Не вкладывайте определения компонентов друг в друга, иначе вы случайно сбросите состояние.
Try out some challenges
Challenge 1 of 5:Fix disappearing input text #
This example shows a message when you press the button. However, pressing the button also accidentally resets the input. Why does this happen? Fix it so that pressing the button does not reset the input text.
