v19.2Latest

Сохранение и сброс состояния

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

Вы узнаете
  • Когда React решает сохранить или сбросить состояние
  • Как заставить React сбросить состояние компонента
  • Как ключи и типы влияют на сохранение состояния

Состояние привязано к позиции в дереве рендеринга

React строитдеревья рендерингадля структуры компонентов в вашем пользовательском интерфейсе.

Когда вы добавляете компоненту состояние, вы можете думать, что состояние «живет» внутри компонента. Но на самом деле состояние хранится внутри React. React связывает каждый фрагмент состояния, который он хранит, с правильным компонентом, основываясь на том, где этот компонент находится в дереве рендеринга.

Здесь есть только один JSX-тег<Counter />, но он рендерится в двух разных позициях:

Вот как это выглядит в виде дерева:

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

Дерево React

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

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

Попробуйте нажать на оба счетчика и обратите внимание, что они не влияют друг на друга:

Как видите, при обновлении одного счетчика обновляется только состояние этого компонента:

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

Обновление состояния

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

Обратите внимание, как в момент, когда вы перестаете рендерить второй счетчик, его состояние полностью исчезает. Это происходит потому, что когда React удаляет компонент, он уничтожает его состояние.

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

Удаление компонента

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

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

Добавление компонента

React сохраняет состояние компонента до тех пор, пока он отображается на своей позиции в дереве пользовательского интерфейса.Если он удаляется или на той же позиции отображается другой компонент, React отбрасывает его состояние.

Один и тот же компонент на одной и той же позиции сохраняет состояние

В этом примере есть два разных тега<Counter />:

Когда вы отмечаете или снимаете флажок, состояние счетчика не сбрасывается. Независимо от того,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 это один и тот же счетчик.

Подводный камень

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

Вы могли ожидать, что состояние сбросится при установке флажка, но этого не происходит! Это потому, чтооба этих тега<Counter />отображаются на одной и той же позиции.React не знает, где вы размещаете условия в своей функции. Все, что он «видит», — это дерево, которое вы возвращаете.

В обоих случаях компонентAppвозвращает<div> с <Counter />в качестве первого дочернего элемента. Для React эти два счетчика имеют один и тот же «адрес»: первый дочерний элемент первого дочернего элемента корня. Именно так React сопоставляет их между предыдущим и следующим рендерами, независимо от того, как вы структурируете свою логику.

Разные компоненты на одной и той же позиции сбрасывают состояние

В этом примере установка флажка заменит<Counter> на <p>:

Здесь вы переключаетесь междуразнымитипами компонентов на одной и той же позиции. Изначально первый дочерний элемент<div>содержалCounter. Но когда вы подставилиp, React удалилCounterиз дерева пользовательского интерфейса и уничтожил его состояние.

Диаграмма с тремя секциями, между которыми переходы обозначены стрелками. Первая секция содержит React-компонент с меткой 'div' и единственным дочерним элементом с меткой 'Counter', содержащим пузырь состояния с меткой 'count' и значением 3. Средняя секция имеет того же родителя 'div', но дочерний компонент теперь удалён, что обозначено жёлтым изображением 'proof'. Третья секция снова имеет того же родителя 'div', теперь с новым дочерним элементом с меткой 'p', выделенным жёлтым.Диаграмма с тремя секциями, между которыми переходы обозначены стрелками. Первая секция содержит React-компонент с меткой 'div' и единственным дочерним элементом с меткой 'Counter', содержащим пузырь состояния с меткой 'count' и значением 3. Средняя секция имеет того же родителя 'div', но дочерний компонент теперь удалён, что обозначено жёлтым изображением 'proof'. Третья секция снова имеет того же родителя 'div', теперь с новым дочерним элементом с меткой 'p', выделенным жёлтым.

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

Диаграмма с тремя секциями, между которыми переходы обозначены стрелками. Первая секция содержит React-компонент с меткой 'p'. Средняя секция имеет того же родителя 'div', но дочерний компонент теперь удалён, что обозначено жёлтым изображением 'proof'. Третья секция снова имеет того же родителя 'div', теперь с новым дочерним элементом с меткой 'Counter', содержащим пузырь состояния с меткой 'count' и значением 0, выделенным жёлтым.Диаграмма с тремя секциями, между которыми переходы обозначены стрелками. Первая секция содержит React-компонент с меткой 'p'. Средняя секция имеет того же родителя 'div', но дочерний компонент теперь удалён, что обозначено жёлтым изображением 'proof'. Третья секция снова имеет того же родителя 'div', теперь с новым дочерним элементом с меткой 'Counter', содержащим пузырь состояния с меткой 'count' и значением 0, выделенным жёлтым.

При обратном переключенииpудаляется, аCounterдобавляется

Кроме того,при рендеринге другого компонента на той же позиции сбрасывается состояние всего его поддерева.Чтобы увидеть, как это работает, увеличьте счётчик, а затем установите флажок:

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

Диаграмма с тремя секциями, между которыми переходы обозначены стрелками. Первая секция содержит React-компонент с меткой 'div' с единственным дочерним элементом с меткой 'section', который имеет единственный дочерний элемент с меткой 'Counter', содержащий пузырь состояния с меткой 'count' и значением 3. Средняя секция имеет того же родителя 'div', но дочерние компоненты теперь удалены, что обозначено жёлтым изображением 'proof'. Третья секция снова имеет того же родителя 'div', теперь с новым дочерним элементом с меткой 'div', выделенным жёлтым, также с новым дочерним элементом с меткой 'Counter', содержащим пузырь состояния с меткой 'count' и значением 0, всё выделено жёлтым.Диаграмма с тремя секциями, между которыми переходы обозначены стрелками. Первая секция содержит React-компонент с меткой 'div' с единственным дочерним элементом с меткой 'section', который имеет единственный дочерний элемент с меткой 'Counter', содержащий пузырь состояния с меткой 'count' и значением 3. Средняя секция имеет того же родителя 'div', но дочерние компоненты теперь удалены, что обозначено жёлтым изображением 'proof'. Третья секция снова имеет того же родителя 'div', теперь с новым дочерним элементом с меткой 'div', выделенным жёлтым, также с новым дочерним элементом с меткой 'Counter', содержащим пузырь состояния с меткой 'count' и значением 0, всё выделено жёлтым.

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

Диаграмма с тремя секциями, между которыми переходы обозначены стрелками. Первая секция содержит React-компонент с меткой 'div', у которого есть один дочерний элемент с меткой 'div', который, в свою очередь, имеет один дочерний элемент с меткой 'Counter', содержащий пузырь состояния с меткой 'count' и значением 0. Средняя секция имеет того же родителя 'div', но дочерние компоненты теперь удалены, что показано желтым изображением 'proof'. Третья секция снова имеет того же родителя 'div', теперь с новым дочерним элементом с меткой 'section', выделенным желтым, также с новым дочерним элементом с меткой 'Counter', содержащим пузырь состояния с меткой 'count' и значением 0, все выделено желтым.Диаграмма с тремя секциями, между которыми переходы обозначены стрелками. Первая секция содержит React-компонент с меткой 'div', у которого есть один дочерний элемент с меткой 'div', который, в свою очередь, имеет один дочерний элемент с меткой 'Counter', содержащий пузырь состояния с меткой 'count' и значением 0. Средняя секция имеет того же родителя 'div', но дочерние компоненты теперь удалены, что показано желтым изображением 'proof'. Третья секция снова имеет того же родителя 'div', теперь с новым дочерним элементом с меткой 'section', выделенным желтым, также с новым дочерним элементом с меткой 'Counter', содержащим пузырь состояния с меткой 'count' и значением 0, все выделено желтым.

При переключении обратноdivудаляется и добавляется новыйsection.

Как правило,если вы хотите сохранить состояние между повторными рендерами, структура вашего дерева должна «совпадать»от одного рендера к другому. Если структура отличается, состояние уничтожается, потому что React уничтожает состояние, когда удаляет компонент из дерева.

Подводный камень

Вот почему не следует вкладывать определения функций компонентов.

Здесь функция компонентаMyTextFieldопределенавнутриMyComponent:

Каждый раз при нажатии кнопки состояние ввода исчезает! Это происходит потому, что для каждого рендерадругаяMyTextField создаётся MyComponent. Вы рендеритедругойкомпонент в той же позиции, поэтому React сбрасывает всё состояние ниже. Это приводит к ошибкам и проблемам с производительностью. Чтобы избежать этой проблемы,всегда объявляйте функции компонентов на верхнем уровне и не вкладывайте их определения.

Сброс состояния в той же позиции

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

В настоящее время при смене игрока счёт сохраняется. ДваCounterа отображаются в одной и той же позиции, поэтому React видит их какодин и тот жеCounter, у которого изменился пропсperson.

Но концептуально в этом приложении они должны быть двумя отдельными счётчиками. Они могут появляться в одном и том же месте в пользовательском интерфейсе, но один — это счётчик для Тейлора, а другой — счётчик для Сары.

Есть два способа сбросить состояние при переключении между ними:

  1. Рендерить компоненты в разных позициях
  2. Дать каждому компоненту явный идентификатор с помощьюkey

Вариант 1: Рендеринг компонента в разных позициях

Если вы хотите, чтобы эти дваCounterа были независимыми, вы можете отрендерить их в двух разных позициях:

  • ИзначальноisPlayerAравноtrue. Поэтому первая позиция содержит состояниеCounter, а вторая пуста.
  • Когда вы нажимаете кнопку «Next player», первая позиция очищается, но теперь вторая содержит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.

Нажатие «next»

Диаграмма с деревом компонентов React. Родительский компонент помечен как 'Scoreboard' с пузырём состояния isPlayerA со значением 'true'. Пузырь состояния выделен жёлтым, что указывает на его изменение. Слева появился новый дочерний компонент, выделенный жёлтым, что указывает на его добавление. Новый дочерний компонент помечен как 'Counter' и содержит пузырь состояния 'count' со значением 0. Правый дочерний компонент заменён изображением жёлтого 'poof', указывающим на его удаление.Диаграмма с деревом компонентов React. Родительский компонент помечен как 'Scoreboard' с пузырём состояния isPlayerA со значением 'true'. Пузырь состояния выделен жёлтым, что указывает на его изменение. Слева появился новый дочерний компонент, выделенный жёлтым, что указывает на его добавление. Новый дочерний компонент помечен как 'Counter' и содержит пузырь состояния 'count' со значением 0. Правый дочерний компонент заменён изображением жёлтого 'poof', указывающим на его удаление.

Нажатие «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-элементы вместо их повторного использования.

Теперь переключение получателя всегда очищает текстовое поле:

Deep Dive
Сохранение состояния для удалённых компонентов

Итоги

  • 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.


Сохранение и сброс состояния | React Learn - Reflow Hub