Сохранение и сброс состояния¶
Состояние изолировано между компонентами. React отслеживает, какое состояние принадлежит тому или иному компоненту, основываясь на их месте в дереве пользовательского интерфейса. Вы можете контролировать, когда сохранять состояние, а когда сбрасывать его между повторными рендерами.
Вы узнаете
- Как React "видит" структуры компонентов
- Когда React решает сохранить или сбросить состояние
- Как заставить React сбросить состояние компонента
- Как ключи и типы влияют на сохранение состояния
Дерево пользовательского интерфейса¶
Браузеры используют множество древовидных структур для моделирования пользовательского интерфейса. DOM представляет элементы HTML, CSSOM делает то же самое для CSS. Есть даже Accessibility tree!
React также использует древовидные структуры для управления и моделирования пользовательского интерфейса. React создает деревья пользовательского интерфейса из вашего JSX. Затем React DOM обновляет элементы DOM браузера в соответствии с этим деревом пользовательского интерфейса. (React Native переводит эти деревья в элементы, специфичные для мобильных платформ).
Из компонентов React создает дерево UI, которое React DOM использует для рендеринга DOM
Состояние привязано к позиции в дереве¶
Когда вы передаете компоненту состояние, вы можете подумать, что это состояние "живет" внутри компонента. Но на самом деле состояние хранится внутри React. React связывает каждую часть состояния, которую он хранит, с нужным компонентом по тому, где этот компонент находится в дереве пользовательского интерфейса.
Здесь есть только один JSX-тег <Counter />
, но он отображается в двух разных позициях:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
|
Вот как они выглядят в виде дерева:
Дерево React
Это два отдельных счетчика, потому что каждый из них отображается в своей позиции в дереве. Обычно вам не нужно думать об этих позициях, чтобы использовать React, но может быть полезно понять, как это работает.
В React каждый компонент на экране имеет полностью изолированное состояние. Например, если вы отобразите два компонента Counter
рядом друг с другом, каждый из них получит свои собственные, независимые состояния score
и hover
.
Попробуйте нажать на оба счетчика и заметите, что они не влияют друг на друга:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
|
Как вы можете видеть, при обновлении одного счетчика обновляется только состояние этого компонента:
Обновление состояния
React будет сохранять состояние до тех пор, пока вы рендерите один и тот же компонент в одной и той же позиции. Чтобы увидеть это, увеличьте оба счетчика, затем удалите второй компонент, сняв флажок "Render the second counter", а затем добавьте его обратно, снова установив флажок:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
|
Обратите внимание, что в момент, когда вы прекращаете рендеринг второго счетчика, его состояние полностью исчезает. Это потому, что когда React удаляет компонент, он уничтожает его состояние.
Удаление компонента
Когда вы отметите "Render the second counter", второй Counter
и его состояние инициализируются с нуля (score = 0
) и добавляются в DOM.
Добавление компонента
React сохраняет состояние компонента до тех пор, пока он отображается в своей позиции в дереве пользовательского интерфейса. Если компонент удаляется, или другой компонент отображается в той же позиции, React удаляет его состояние.
Тот же компонент в той же позиции сохраняет состояние¶
В этом примере есть два разных тега <Counter />
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
|
Когда вы устанавливаете или снимаете флажок, состояние счетчика не сбрасывается. Независимо от того, является ли isFancy
true
или false
, у вас всегда будет <Counter />
в качестве первого дочернего элемента div
, возвращаемого из корневого компонента App
:
Обновление состояния App
не сбрасывает Counter
, потому что Counter
остается в том же положении
Это тот же компонент в той же позиции, поэтому с точки зрения React, это тот же счетчик.
Внимание
Помните, что для React важна позиция в дереве пользовательского интерфейса, а не в JSX-разметке! Этот компонент имеет два предложения return
с разными JSX-тегами <Counter />
внутри и вне if
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
|
Можно было бы ожидать, что состояние сбросится, когда вы поставите галочку, но этого не происходит! Это происходит потому, что оба этих тега <Counter />
отображаются в одной и той же позиции. React не знает, где вы размещаете условия в вашей функции. Все, что он "видит" - это дерево, которое вы возвращаете.
В обоих случаях компонент App
возвращает <div>
с <Counter />
в качестве первого дочернего элемента. Для React эти два счетчика имеют одинаковый "адрес": первый ребенок первого ребенка корня. Вот как React сопоставляет их между предыдущим и следующим рендерами, независимо от того, как вы структурируете свою логику.
Разные компоненты в одной и той же позиции сбрасывают состояние¶
В этом примере установка флажка заменит <Counter>
на <p>
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
|
Здесь вы переключаетесь между различными типами компонентов в одной и той же позиции. Изначально первый дочерний компонент <div>
содержал Counter
. Но когда вы поменяли местами p
, React удалил Counter
из дерева пользовательского интерфейса и уничтожил его состояние.
Когда Counter
меняется на p
, Counter
удаляется, а p
добавляется
При обратном переключении p
удаляется, а Counter
добавляется
Также, когда вы отображаете другой компонент в той же позиции, он сбрасывает состояние всего своего поддерева. Чтобы увидеть, как это работает, увеличьте счетчик, а затем установите флажок:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
|
Состояние счетчика сбрасывается, когда вы нажимаете на флажок. Хотя вы отображаете Counter
, первый ребенок div
меняется с div
на секцию
. Когда дочерний div
был удален из DOM, все дерево под ним (включая Counter
и его состояние) также было уничтожено.
Когда section
меняется на div
, section
удаляется и добавляется новый div
.
При обратном переключении, div
удаляется, а новый секция
добавляется
Как правило, если вы хотите сохранить состояние между повторными рендерами, структура дерева должна "совпадать " от одного рендера к другому. Если структура отличается, состояние будет уничтожено, потому что React уничтожает состояние, когда удаляет компонент из дерева.
Внимание
Вот почему не следует вставлять определения функций компонентов.
Здесь функция компонента MyTextField
определена внутри MyComponent
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
|
Каждый раз, когда вы нажимаете на кнопку, состояние ввода исчезает! Это происходит потому, что различная функция MyTextField
создается для каждого рендера MyComponent
. Вы рендерите разный компонент в той же позиции, поэтому React сбрасывает все состояние ниже. Это приводит к ошибкам и проблемам с производительностью. Чтобы избежать этой проблемы, всегда объявляйте функции компонента на верхнем уровне и не вкладывайте их определения.
Сброс состояния в одной и той же позиции¶
По умолчанию React сохраняет состояние компонента, пока он остается в той же позиции. Обычно это именно то, что вам нужно, поэтому это имеет смысл как поведение по умолчанию. Но иногда вам может понадобиться сбросить состояние компонента. Рассмотрим это приложение, позволяющее двум игрокам следить за своими результатами во время каждого хода:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
|
В настоящее время, когда вы меняете игрока, счет сохраняется. Два счетчика
появляются в одной и той же позиции, поэтому React воспринимает их как один и тот же счетчик
, чей параметр персона
изменился.
Но концептуально в этом приложении они должны быть двумя отдельными счетчиками. Они могут появляться в одном и том же месте пользовательского интерфейса, но один из них будет счетчиком для Тейлора, а другой - для Сары.
Есть два способа сбросить состояние при переключении между ними:
- Рендерить компоненты в разных позициях
- Придать каждому компоненту явную идентичность с помощью
key
.
Вариант 1: Рендеринг компонента в разных позициях¶
Если вы хотите, чтобы эти два Counter
были независимыми, вы можете отобразить их в двух разных позициях:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
|
- Изначально
isPlayerA
имеет значениеtrue
. Поэтому первая позиция содержит состояниеCounter
, а вторая пуста. - Когда вы нажимаете кнопку "Следующий игрок", первая позиция очищается, но вторая теперь содержит
Counter
.
Начальное состояние
Нажимаем "далее"
Снова нажимаем "далее"
Состояние каждого Counter
уничтожается каждый раз, когда он удаляется из DOM. Вот почему они обнуляются каждый раз, когда вы нажимаете на кнопку.
Это решение удобно, когда у вас есть только несколько независимых компонентов, отображаемых в одном месте. В этом примере их всего два, поэтому нет необходимости рендерить оба отдельно в JSX.
Вариант 2: Сброс состояния с помощью ключа¶
Существует и другой, более общий способ сброса состояния компонента.
Вы могли видеть key
при рендеринге списков. Ключи нужны не только для списков! Вы можете использовать ключи, чтобы заставить React различать любые компоненты. По умолчанию React использует порядок внутри родителя ("первый счетчик", "второй счетчик") для различения компонентов. Но ключи позволяют сообщить React, что это не просто первый счетчик или второй счетчик, а конкретный счетчик - например, счетчик Тейлора. Таким образом, React будет знать счетчик Тейлора, где бы он ни появился в дереве!
В этом примере два <Counter />
не имеют общего состояния, хотя они появляются в одном и том же месте в JSX:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
|
При переключении между Тейлором и Сарой состояние не сохраняется. Это происходит потому, что вы дали им разные "ключи":
1 2 3 4 5 6 7 |
|
Указание key
говорит React использовать сам key
как часть позиции, а не их порядок внутри родителя. Вот почему, даже если вы отображаете их в одном и том же месте в JSX, React воспринимает их как два разных счетчика, и поэтому они никогда не будут иметь общего состояния. Каждый раз, когда счетчик появляется на экране, создается его состояние. Каждый раз, когда он удаляется, его состояние уничтожается. Переключение между ними сбрасывает их состояние снова и снова.
Помните, что ключи не являются глобально уникальными. Они определяют только позицию в пределах родителя.
Сброс формы с помощью ключа¶
Сброс состояния с помощью ключа особенно полезен при работе с формами.
В этом приложении для чата компонент <Chat>
содержит состояние ввода текста:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Попробуйте ввести что-нибудь в поле ввода, а затем нажмите "Алиса" или "Боб", чтобы выбрать другого адресата. Вы заметите, что состояние ввода сохраняется, потому что <Chat>
отображается в той же позиции в дереве.
В многих приложениях это может быть желаемым поведением, но не в приложении чата! Вы же не хотите, чтобы пользователь отправил сообщение, которое он уже набрал, не тому человеку из-за случайного нажатия. Чтобы исправить это, добавьте key
:
1 |
|
Это гарантирует, что при выборе другого получателя компонент Chat
будет воссоздан с нуля, включая все состояния в дереве под ним. React также создаст элементы DOM заново, вместо того чтобы использовать их повторно.
Теперь переключение получателя всегда очищает текстовое поле:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Сохранение состояния для удаленных компонентов
В реальном приложении для чата вы, вероятно, захотите восстановить состояние ввода, когда пользователь снова выберет предыдущего получателя. Есть несколько способов сохранить состояние "живым" для компонента, который больше не виден:
- Вы можете отобразить все чаты, а не только текущий, но скрыть все остальные с помощью CSS. Чаты не будут удалены из дерева, поэтому их локальное состояние будет сохранено. Это решение отлично подходит для простых пользовательских интерфейсов. Но оно может стать очень медленным, если скрытые деревья большие и содержат много узлов DOM.
- Можно поднять состояние вверх и хранить ожидающее сообщение для каждого получателя в родительском компоненте. Таким образом, когда дочерние компоненты будут удалены, это не будет иметь значения, потому что важная информация будет храниться в родительском компоненте. Это наиболее распространенное решение.
- Вы также можете использовать другой источник в дополнение к React state. Например, вы, вероятно, хотите, чтобы черновик сообщения сохранялся, даже если пользователь случайно закроет страницу. Чтобы реализовать это, вы можете заставить компонент
Chat
инициализировать свое состояние, читая изlocalStorage
, и сохранять черновики там же.
Независимо от того, какую стратегию вы выберете, чат с Алисой концептуально отличается от чата с Бобом, поэтому имеет смысл дать ключ
дереву <Чат>
, основанный на текущем получателе.
Итого
- React сохраняет состояние до тех пор, пока один и тот же компонент отображается в одной и той же позиции.
- Состояние не хранится в JSX-тегах. Оно связано с позицией дерева, в которую вы поместили JSX.
- Вы можете заставить поддерево сбросить свое состояние, задав ему другой ключ.
- Не вставляйте определения компонентов, иначе вы случайно сбросите состояние.
Задачи¶
1. Исправление исчезающего текста ввода¶
Этот пример показывает сообщение при нажатии на кнопку. Однако при нажатии кнопки также происходит случайный сброс ввода. Почему так происходит? Исправьте, чтобы нажатие кнопки не сбрасывало вводимый текст.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
|
Показать решение
Проблема в том, что Form
отображается в разных позициях. В ветке if
она является вторым дочерним элементом div
, а в ветке else
- первым. Поэтому тип компонента в каждой позиции меняется. Первая позиция меняется между p
и Form
, а вторая позиция меняется между Form
и button
. React сбрасывает состояние каждый раз, когда меняется тип компонента.
Самое простое решение - объединить ветви, чтобы Form
всегда отображалась в одной и той же позиции:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
|
Технически, вы также можете добавить null
перед <Form />
в ветке else
, чтобы соответствовать структуре ветки if
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
|
Таким образом, Form
всегда является вторым дочерним элементом, поэтому он остается в той же позиции и сохраняет свое состояние. Но этот подход гораздо менее очевиден и создает риск того, что кто-то другой удалит этот null
.
2. Поменять местами два поля формы¶
Эта форма позволяет вводить имя и фамилию. В ней также есть флажок, контролирующий, какое поле будет первым. Если установить флажок, поле "Фамилия" появится перед полем "Имя".
Это почти работает, но есть ошибка. Если вы заполните поле "Имя" и установите флажок, текст останется в первом поле (теперь это "Фамилия"). Исправьте это так, чтобы при изменении порядка ввода текст также перемещался.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
|
Показать подсказку
Похоже, что для этих полей недостаточно их положения внутри родительского поля. Есть ли какой-то способ указать React, как сопоставить состояние между повторными рендерами?
Показать решение
Дайте key
обоим компонентам <Field>
в ветвях if
и else
. Это подскажет React, как "подобрать" правильное состояние для любого из <Полей>
, даже если их порядок в родительском компоненте изменится:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
|
3. Сброс детальной формы¶
Это редактируемый список контактов. Вы можете редактировать данные выбранного контакта, а затем либо нажать "Сохранить", чтобы обновить его, либо "Сбросить", чтобы отменить изменения.
Когда вы выбираете другой контакт (например, Алису), состояние обновляется, но форма продолжает показывать данные предыдущего контакта. Исправьте это так, чтобы форма сбрасывалась при изменении выбранного контакта.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
|
Показать решение
Дайте key={selectedId}
компоненту EditContact
. Таким образом, при переключении между разными контактами форма будет перезагружаться:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
|
4. Очистить изображение во время его загрузки¶
Когда вы нажимаете кнопку "Далее", браузер начинает загрузку следующего изображения. Однако, поскольку оно отображается в том же теге img
, по умолчанию вы будете видеть предыдущее изображение, пока не загрузится следующее. Это может быть нежелательно, если важно, чтобы текст всегда совпадал с изображением. Измените это так, чтобы при нажатии кнопки "Next" предыдущее изображение сразу же убиралось.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
|
Показать подсказку
Есть ли способ указать React на повторное создание DOM вместо его повторного использования?
Показать решение
Вы можете указать key
для тега img
. Когда этот key
изменится, React заново создаст DOM-узел img
с нуля. Это вызывает кратковременную вспышку при загрузке каждого изображения, поэтому это не то, что вы хотели бы делать для каждого изображения в вашем приложении. Но это имеет смысл, если вы хотите, чтобы изображение всегда соответствовало тексту.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
|
5. Исправьте неуместное состояние в списке¶
В этом списке каждый Contact
имеет состояние, которое определяет, была ли для него нажата галочка "Показать почту". Нажмите "Показать почту" для Алисы, а затем установите флажок "Показывать в обратном порядке". Вы заметите, что письмо Тейлора теперь развернуто, а письмо Алисы, которое переместилось в самый низ, кажется свернутым.
Исправьте это так, чтобы развернутое состояние было связано с каждым контактом, независимо от выбранного порядка.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
Показать решение
Проблема в том, что в этом примере в качестве key
использовался индекс:
1 2 3 |
|
Однако вы хотите, чтобы состояние было связано с каждым конкретным контактом.
Использование идентификатора контакта в качестве key
устраняет проблему:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
Состояние ассоциируется с позицией дерева. Ключ key
позволяет указать именованную позицию вместо того, чтобы полагаться на порядок.
Источник — https://react.dev/learn/preserving-and-resetting-state