Реагирование на ввод с помощью состояния¶
React предоставляет декларативный способ манипулирования пользовательским интерфейсом. Вместо того чтобы напрямую управлять отдельными частями пользовательского интерфейса, вы описываете различные состояния, в которых может находиться ваш компонент, и переключаетесь между ними в ответ на ввод пользователя. Это похоже на то, как дизайнеры думают о пользовательском интерфейсе.
Вы узнаете
- Чем декларативное программирование пользовательского интерфейса отличается от императивного программирования пользовательского интерфейса
- Как перечислить различные визуальные состояния, в которых может находиться ваш компонент
- Как вызвать изменения между различными визуальными состояниями из кода
Как декларативный пользовательский интерфейс отличается от императивного¶
Когда вы проектируете взаимодействие пользовательского интерфейса, вы, вероятно, думаете о том, как пользовательский интерфейс изменяется в ответ на действия пользователя. Рассмотрим форму, которая позволяет пользователю отправить ответ:
- Когда вы вводите что-то в форму, кнопка "Отправить" становится активной.
- Когда вы нажимаете кнопку "Отправить", и форма, и кнопка отключаются, и появляется волчок.
- Если сетевой запрос прошел успешно, форма спрячется, и появится сообщение "Спасибо".
- Если сетевой запрос не удался, появляется сообщение об ошибке, и форма снова становится открытой.
В императивном программировании вышесказанное прямо соответствует тому, как вы реализуете взаимодействие. Вы должны написать точные инструкции для манипулирования пользовательским интерфейсом в зависимости от того, что только что произошло. Вот еще один способ подумать об этом: представьте, что вы едете рядом с кем-то в машине и говорите ему пошагово, куда ехать.
Они не знают, куда вы хотите поехать, они просто следуют вашим командам. (И если вы ошибетесь в указаниях, вы окажетесь не в том месте!) Это называется императивным, потому что вы должны "командовать" каждым элементом, от спиннера до кнопки, указывая компьютеру как обновить пользовательский интерфейс.
В этом примере императивного программирования пользовательского интерфейса форма построена без 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 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 64 65 66 67 68 69 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
Манипулирование пользовательским интерфейсом в императивном порядке достаточно хорошо работает для отдельных примеров, но в более сложных системах управлять им становится экспоненциально сложнее. Представьте себе обновление страницы, полной различных форм, как эта. Добавление нового элемента пользовательского интерфейса или нового взаимодействия потребует тщательной проверки всего существующего кода, чтобы убедиться, что вы не внесли ошибку (например, забыли показать или скрыть что-то).
React был создан для решения этой проблемы.
В React вы не управляете пользовательским интерфейсом напрямую - то есть не включаете, не отключаете, не показываете и не скрываете компоненты. Вместо этого вы заявляете, что вы хотите показать, а React сам решает, как обновить пользовательский интерфейс. Подумайте о том, чтобы сесть в такси и сказать водителю, куда вы хотите поехать, вместо того, чтобы сказать ему, куда именно повернуть. Это работа водителя - доставить вас туда, и он может даже знать некоторые короткие пути, о которых вы не подумали!
Размышления о декларативном интерфейсе¶
Вы уже видели выше, как реализовать форму императивно. Чтобы лучше понять, как мыслить в React, ниже вы пройдете через повторную реализацию этого пользовательского интерфейса в React:
- Определите различные визуальные состояния вашего компонента
- Определите, что вызывает изменение состояния.
- Представьте состояние в памяти с помощью
useState
. - Удалите любые несущественные переменные состояния
- Подключите обработчики событий для установки состояния
Шаг 1: Определите различные визуальные состояния вашего компонента¶
В компьютерных науках вы можете услышать о том, что "машина состояний" находится в одном из нескольких "состояний". Если вы работаете с дизайнером, вы могли видеть макеты для различных "визуальных состояний". React стоит на пересечении дизайна и информатики, поэтому обе эти идеи являются источниками вдохновения.
Во-первых, вам нужно представить все различные "состояния" пользовательского интерфейса, которые может увидеть пользователь:
- Пусто: Форма имеет отключенную кнопку "Отправить".
- Типирование: Форма имеет включенную кнопку "Отправить".
- Отправка: Форма полностью отключена. Отображается спиннер.
- Успех: Вместо формы отображается сообщение "Спасибо".
- Ошибка: То же самое, что и состояние типирования, но с дополнительным сообщением об ошибке.
Как и дизайнеру, вам нужно "смоделировать" или создать "макеты" для различных состояний, прежде чем добавлять логику. Например, здесь показан макет только для визуальной части формы. Этот макет управляется параметром status
со значением по умолчанию 'empty'
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
Вы можете назвать этот пропс как угодно, именование не имеет значения. Попробуйте изменить status = 'empty'
на status = 'success'
, чтобы увидеть появление сообщения об успехе. Мокинг позволяет вам быстро проработать пользовательский интерфейс, прежде чем вы подключите какую-либо логику. Вот более подробный прототип того же компонента, все еще "управляемый" пропсом status
:
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 |
|
Отображение множества визуальных состояний одновременно
Если компонент имеет много визуальных состояний, может быть удобно показать их все на одной странице:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
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 |
|
Подобные страницы часто называют “живыми styleguides” или “storybooks”.
Шаг 2: Определите, что вызывает эти изменения состояния¶
Вы можете инициировать обновление состояния в ответ на два вида входных данных:
- человеческие входы, такие как нажатие кнопки, ввод текста в поле, переход по ссылке.
- Компьютерные данные, такие как получение ответа от сети, завершение тайм-аута, загрузка изображения.
В обоих случаях вы должны установить переменные состояния для обновления пользовательского интерфейса. Для разрабатываемой вами формы вам нужно будет изменять состояние в ответ на несколько различных входов:
- Изменение текстового ввода (человек) должно переводить его из состояния Empty в состояние Typing или обратно, в зависимости от того, пустое текстовое поле или нет.
- Нажатие кнопки "Отправить " (человек) должно перевести его в состояние Отправить.
- Успешный ответ сети (компьютер) должен перевести его в состояние Успех.
- Неуспешный сетевой ответ (компьютер) должен перевести его в состояние Ошибка с соответствующим сообщением об ошибке.
Обратите внимание, что человеческий ввод часто требует обработчиков событий!
Чтобы помочь визуализировать этот поток, попробуйте нарисовать на бумаге каждое состояние в виде круга с меткой, а каждое изменение между двумя состояниями - в виде стрелки. Таким образом можно набросать множество потоков и отсеять ошибки задолго до внедрения.
Состояние формы
Шаг 3: Представьте состояние в памяти с помощью useState
¶
Далее вам нужно будет представить визуальные состояния вашего компонента в памяти с помощью useState
. Простота - это ключ: каждая часть состояния - это "движущаяся часть", и вы хотите как можно меньше "движущихся частей". Больше сложности приводит к большему количеству ошибок!
Начните с состояния, которое абсолютно должно быть там. Например, вам нужно хранить ответ
для ввода, и ошибку
(если она существует) для хранения последней ошибки:
1 2 |
|
Затем вам понадобится переменная состояния, представляющая одно из визуальных состояний, которое вы хотите отобразить. Обычно существует более чем один способ представить это в памяти, поэтому вам придется поэкспериментировать с этим.
Если вам трудно сразу придумать лучший способ, начните с добавления достаточного количества состояний, чтобы вы были определенно уверены, что все возможные визуальные состояния охвачены:
1 2 3 4 5 |
|
Ваша первая идея, скорее всего, не будет лучшей, но это нормально - рефакторинг состояния является частью процесса!
Шаг 4: Удалите все несущественные переменные состояния¶
Вы хотите избежать дублирования в содержимом состояния, чтобы отслеживать только то, что необходимо. Потратив немного времени на рефакторинг структуры состояния, вы сделаете ваши компоненты более понятными, сократите дублирование и избежите непреднамеренных значений. Ваша цель - предотвратить случаи, когда состояние в памяти не представляет никакого действительного пользовательского интерфейса, который вы хотели бы видеть у пользователя (например, вы никогда не захотите показывать сообщение об ошибке и одновременно отключать ввод, иначе пользователь не сможет исправить ошибку!)
Вот несколько вопросов, которые можно задать переменным состояния:
- Приводит ли это состояние к парадоксу? Например,
isTyping
иisSubmitting
не могут быть одновременноtrue
. Парадокс обычно означает, что состояние недостаточно ограничено. Существует четыре возможных комбинации двух булевых чисел, но только три соответствуют допустимым состояниям. Чтобы убрать "невозможное" состояние, вы можете объединить их вstatus
, который должен быть одним из трех значений:'typing'
,'submitting'
или'success'
. - Есть ли та же информация в другой переменной состояния? Еще один парадокс:
isEmpty
иisTyping
не могут быть одновременноtrue
. Делая их отдельными переменными состояния, вы рискуете рассинхронизировать их и вызвать ошибки. К счастью, вы можете убратьisEmpty
и вместо этого проверятьanswer.length === 0
. - Можно ли получить ту же информацию из обратной переменной состояния?
isError
не нужен, потому что вместо него можно проверитьerror !== null
.
После этой чистки у вас осталось 3 (меньше 7!) существенных переменных состояния:
1 2 3 4 5 |
|
Вы знаете, что они важны, потому что вы не можете удалить ни одно из них без нарушения функциональности.
Устранение невозможных состояний с помощью редуктора
Эти три переменные достаточно хорошо отображают состояние формы. Однако все еще есть некоторые промежуточные состояния, которые не имеют полного смысла. Например, ненулевая error
не имеет смысла, когда status
- success
. Чтобы смоделировать состояние более точно, вы можете извлечь его в редуктор Редукторы позволяют вам объединить несколько переменных состояния в один объект и консолидировать всю связанную логику!
Шаг 5: Подключите обработчики событий для установки состояния¶
Наконец, создайте обработчики событий, которые будут обновлять состояние. Ниже показана окончательная форма с подключенными обработчиками событий:
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 64 65 66 67 68 69 70 71 72 73 74 75 |
|
Хотя этот код длиннее, чем оригинальный императивный пример, он гораздо менее хрупок. Выражение всех взаимодействий в виде изменений состояния позволяет впоследствии вводить новые визуальные состояния без нарушения существующих. Это также позволяет изменять то, что должно отображаться в каждом состоянии, не меняя логику самого взаимодействия.
Итого
- Декларативное программирование означает описание пользовательского интерфейса для каждого визуального состояния, а не микроменеджмент пользовательского интерфейса (императивное программирование).
- При разработке компонента:
- Определите все его визуальные состояния.
- Определите человеческие и компьютерные триггеры для изменения состояния.
- Смоделируйте состояние с помощью
useState
. - Удалите несущественные состояния, чтобы избежать ошибок и парадоксов.
- Подключите обработчики событий для установки состояния.
Задачи¶
1. Добавление и удаление класса CSS¶
Сделайте так, чтобы щелчок на картинке удалял CSS-класс background--active
из внешнего <div>
, но добавлял класс picture--active
к <img>
. Повторный щелчок по фону восстановит исходные CSS-классы.
Визуально вы должны увидеть, что щелчок на изображении удаляет фиолетовый фон и выделяет границу изображения. Щелчок за пределами изображения выделяет фон, но убирает выделение границы изображения.
1 2 3 4 5 6 7 8 9 10 11 |
|
Показать решение
Этот компонент имеет два визуальных состояния: когда изображение активно и когда оно неактивно:
- Когда изображение активно, CSS-классами являются
background
иpicture picture--active
. - Когда изображение неактивно, CSS-классами являются
background background--active
иpicture
.
Одной булевой переменной состояния достаточно, чтобы помнить, активно ли изображение. Первоначальная задача заключалась в удалении или добавлении CSS-классов. Однако в React вам нужно описывать то, что вы хотите видеть, а не манипулировать элементами пользовательского интерфейса. Поэтому вам нужно вычислить оба класса CSS на основе текущего состояния. Также необходимо остановить распространение, чтобы щелчок на изображении не регистрировался как щелчок на фоне.
Проверьте, что эта версия работает, щелкнув изображение, а затем за его пределами:
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 |
|
В качестве альтернативы вы можете вернуть два отдельных куска 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 |
|
Помните, что если два разных JSX-куска описывают одно и то же дерево, их вложенность (первый <div>
→ первый <img>
) должна совпадать. В противном случае переключение isActive
приведет к воссозданию всего дерева внизу и сбросу его состояния. Вот почему, если в обоих случаях возвращается одинаковое JSX-дерево, лучше писать их как один кусок JSX.
2. Редактор профиля¶
Вот небольшая форма, реализованная с помощью обычного JavaScript и DOM. Поиграйте с ней, чтобы понять ее поведение:
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 |
|
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 |
|
Эта форма переключается между двумя режимами: в режиме редактирования вы видите вводимые данные, а в режиме просмотра - только результат. Метка кнопки меняется между "Редактировать" и "Сохранить" в зависимости от того, в каком режиме вы находитесь. Когда вы изменяете вводимые данные, приветственное сообщение внизу обновляется в режиме реального времени.
Ваша задача - реализовать это на React в песочнице ниже. Для вашего удобства разметка уже была преобразована в JSX, но вам нужно будет сделать так, чтобы она показывала и скрывала входы, как это делает оригинал.
Убедитесь, что она также обновляет текст внизу!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
Показать решение
Вам понадобятся две переменные состояния для хранения входных значений: firstName
и lastName
. Вам также понадобится переменная состояния isEditing
, которая будет определять, отображать ли вводимые значения или нет. Вам не понадобится переменная fullName
, потому что полное имя всегда может быть вычислено из firstName
и lastName
.
Наконец, вы должны использовать условный рендеринг, чтобы показывать или скрывать входы в зависимости от isEditing
.
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 |
|
Сравните это решение с исходным императивным кодом. Чем они отличаются?
3. Рефакторинг императивного решения без 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 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
|
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 |
|
Представьте, что React не существует. Можете ли вы рефакторить этот код таким образом, чтобы сделать логику менее хрупкой и более похожей на версию React? Как бы это выглядело, если бы состояние было явным, как в React?
Если вам трудно сообразить, с чего начать, то в приведенной ниже заглушке уже есть большая часть структуры. Если вы начнете здесь, заполните недостающую логику в функции updateDOM
. (При необходимости ссылайтесь на исходный код).
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 64 65 66 67 |
|
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 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 64 65 66 67 68 69 70 71 72 73 74 75 76 |
|
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 |
|
Функция updateDOM
, которую вы написали, показывает, что React делает под капотом, когда вы устанавливаете состояние. (Однако React также избегает обращения к DOM для свойств, которые не изменились с момента их последней установки).
Источник — https://react.dev/learn/reacting-to-input-with-state