Реагирование на входные данные с помощью состояния
React предоставляет декларативный способ манипулирования пользовательским интерфейсом. Вместо того, чтобы напрямую изменять отдельные элементы интерфейса, вы описываете различные состояния, в которых может находиться ваш компонент, и переключаетесь между ними в ответ на входные данные пользователя. Это похоже на то, как дизайнеры думают об интерфейсе.
Вы узнаете
- В чем разница между декларативной и императивной программой для интерфейса
- Как перечислить различные визуальные состояния вашего компонента
- Как инициировать изменения между различными визуальными состояниями из кода
Как декларативный интерфейс отличается от императивного
При проектировании взаимодействий с пользователем, вы, вероятно, думаете о том, как интерфейс меняется в ответ на действия пользователя. Рассмотрим форму, позволяющую пользователю ввести ответ:
- Когда вы вводите что-то в форму, кнопка «Отправить» становится активной.
- Когда вы нажимаете «Отправить», и форма, и кнопка становятся неактивными, и появляется спиннер.
- Если сетевой запрос выполнен успешно, форма скрывается, и появляется сообщение «Спасибо».
- Если сетевой запрос завершился ошибкой, появляется сообщение об ошибке, и форма становится активной снова.
В императивной программировании вышеописанное напрямую соответствует тому, как вы реализуете взаимодействие. Вам нужно написать точные инструкции для манипулирования интерфейсом в зависимости от того, что только что произошло. Вот другой способ понять это: представьте, что вы едете рядом с кем-то в машине и даете им указания по поворотам, куда ехать.
Иллюстрировано Речел Ли Наборс
Они не знают, куда вы хотите поехать, они просто выполняют ваши команды. (И если вы ошибетесь с маршрутом, вы окажетесь не там, где нужно!) Это называется императивным, потому что вам нужно «командовать» каждым элементом, от спиннера до кнопки, указывая компьютеру, как обновить интерфейс.
В этом примере императивной программирования для интерфейса форма создана без React. Она использует только браузерный DOM:
async function handleFormSubmit(e) { e.preventDefault(); disable(textarea); disable(button); show(loadingMessage); hide(errorMessage); try { await submitForm(textarea.value); show(successMessage); hide(form); } catch (err) { show(errorMessage); errorMessage.textContent = err.message; } finally { hide(loadingMessage); enable(textarea); enable(button); } } function handleTextareaChange() { if (textarea.value.length === 0) { disable(button); } else { enable(button); } } function hide(el) { el.style.display = 'none'; } function show(el) { el.style.display = ''; } function enable(el) { el.disabled = false; } function disable(el) { el.disabled = true; } function submitForm(answer) { // Pretend it's hitting the network. return new Promise((resolve, reject) => { setTimeout(() => { if (answer.toLowerCase() === 'istanbul') { resolve(); } else { reject(new Error('Good guess but a wrong answer. Try again!')); } }, 1500); }); } let form = document.getElementById('form'); let textarea = document.getElementById('textarea'); let button = document.getElementById('button'); let loadingMessage = document.getElementById('loading'); let errorMessage = document.getElementById('error'); let successMessage = document.getElementById('success'); form.onsubmit = handleFormSubmit; textarea.oninput = handleTextareaChange;
Манипулирование интерфейсом императивным способом достаточно хорошо работает для изолированных примеров, но становится экспоненциально сложнее управлять им в более сложных системах. Представьте себе обновление страницы с множеством таких форм. Добавление нового элемента интерфейса или нового взаимодействия потребует тщательной проверки всего существующего кода, чтобы убедиться, что вы не внесли ошибку (например, забыли что-то показать или скрыть).
React был создан для решения этой проблемы.
В React вы не напрямую манипулируете интерфейсом — это означает, что вы не активируете, деактивируете, показываете или скрываете компоненты напрямую. Вместо этого вы объявляете, что хотите показать, а React определяет, как обновить интерфейс. Представьте, что вы садитесь в такси и говорите водителю, куда хотите поехать, вместо того, чтобы указывать ему каждый поворот. Задача водителя — доставить вас туда, и он может даже знать некоторые сокращения, о которых вы не задумывались!
Иллюстрировано Речел Ли Наборс
Делая интерфейс декларативным
Вы видели, как реализовать форму императивно выше. Чтобы лучше понять, как думать в React, ниже вы пройдете по реализации этого интерфейса в React:
- Определите различные визуальные состояния вашего компонента
- Определите, что вызывает эти изменения состояния
-
Представьте состояние в памяти, используя
useState - Удалите любые несущественные переменные состояния
- Свяжите обработчики событий для установки состояния
Шаг 1: Определите различные визуальные состояния вашего компонента
В информатике вы можете услышать о «машине состояний», которая может находиться в одном из нескольких «состояний». Если вы работаете с дизайнером, вы могли видеть макеты для различных «визуальных состояний». React находится на стыке дизайна и информатики, поэтому обе эти идеи являются источником вдохновения.
Сначала вам нужно визуализировать все возможные «состояния» интерфейса, которые может видеть пользователь:
- Пустое: форма имеет неактивную кнопку «Отправить».
- Ввод текста: форма имеет активную кнопку «Отправить».
- Отправка: форма полностью неактивна. Показан спиннер.
- Успех: вместо формы показано сообщение «Спасибо».
- Ошибка: то же, что и состояние «Ввод текста», но с дополнительным сообщением об ошибке.
Как и дизайнер, вы захотите «набросать» или создать «макеты» для различных состояний, прежде чем добавлять логику. Например, вот макет только визуальной части формы. Этот макет управляется свойством, называемым status со значением по умолчанию 'empty':
export default function Form({ status = 'empty' }) { if (status === 'success') { return <h1>That's right!</h1> } return ( <> <h2>City quiz</h2> <p> In which city is there a billboard that turns air into drinkable water? </p> <form> <textarea /> <br /> <button> Submit </button> </form> </> ) }
Вы можете назвать это свойство как угодно, имя не важно. Попробуйте изменить status = 'empty' на status = 'success' для отображения сообщения об успехе. Макеты позволяют быстро итерировать интерфейс, прежде чем вы свяжете какую-либо логику. Вот более подробный прототип того же компонента, всё ещё «управляемый» свойством status:
export default function Form({ // Try 'submitting', 'error', 'success': status = 'empty' }) { if (status === 'success') { return <h1>That's right!</h1> } return ( <> <h2>City quiz</h2> <p> In which city is there a billboard that turns air into drinkable water? </p> <form> <textarea disabled={ status === 'submitting' } /> <br /> <button disabled={ status === 'empty' || status === 'submitting' }> Submit </button> {status === 'error' && <p className="Error"> Good guess but a wrong answer. Try again! </p> } </form> </> ); }
Подробное изучение
Одновременное отображение многих визуальных состояний
Если у компонента много визуальных состояний, может быть удобно отобразить их все на одной странице:
import Form from './Form.js'; let statuses = [ 'empty', 'typing', 'submitting', 'success', 'error', ]; export default function App() { return ( <> {statuses.map(status => ( <section key={status}> <h4>Form ({status}):</h4> <Form status={status} /> </section> ))} </> ); }
Такие страницы часто называют «живыми стилгайдами» или «книгами историй».
Шаг 2: Определите, что вызывает эти изменения состояния
Вы можете запускать обновления состояния в ответ на два типа входов:
- Входы пользователя, например, нажатие кнопки, ввод текста в поле, навигация по ссылке.
- Входы компьютера, например, получение сетевого ответа, завершение таймаута, загрузка изображения.
Иллюстрировано Речел Ли Наборс
В обоих случаях вы должны установить переменные состояния, чтобы обновить интерфейс. Для разрабатываемой вами формы вам нужно будет изменять состояние в ответ на несколько различных входов:
- Изменение текста ввода (пользовательское) должно переключиться из состояния Пустое в состояние Ввод текста или обратно, в зависимости от того, пусто ли текстовое поле.
- Нажатие кнопки «Отправить» (пользовательское) должно переключиться в состояние Отправка.
- Успешный сетевой ответ (компьютер) должен переключиться в состояние Успех.
- Ошибка сетевого ответа (компьютер) должен переключиться в состояние Ошибка с соответствующим сообщением об ошибке.
Примечание
Обратите внимание, что пользовательские входы часто требуют обработчиков событий!
Для визуализации этого процесса попробуйте нарисовать каждое состояние на бумаге в виде помеченной окружности, а каждое изменение между двумя состояниями в виде стрелки. Таким образом, вы можете набросать множество потоков и выявить ошибки задолго до реализации.
Состояния формы
Шаг 3: Представление состояния в памяти с помощью useState
Далее вам нужно будет представить визуальные состояния вашего компонента в памяти с помощью useState. Простота — залог успеха: каждый элемент состояния — это «подвижная часть», и вы хотите как можно меньше «подвижных частей». Больше сложности — больше ошибок!
Начните с состояния, которое обязательно должно быть. Например, вам нужно будет хранить answer для ввода и error (если оно есть) для хранения последней ошибки:
const [answer, setAnswer] = useState('');
const [error, setError] = useState(null); Затем вам понадобится переменная состояния, представляющая, какое из визуальных состояний вы хотите отобразить. Обычно существует более одного способа представить это в памяти, поэтому вам нужно будет экспериментировать с этим.
Если вам трудно сразу придумать лучший способ, начните с добавления достаточного количества состояний, чтобы вы точно убедились, что все возможные визуальные состояния охвачены:
const [isEmpty, setIsEmpty] = useState(true);
const [isTyping, setIsTyping] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const [isError, setIsError] = useState(false); Ваша первая идея, вероятно, не будет лучшей, но это нормально — переработка состояния — это часть процесса!
Шаг 4: Удалите любые несущественные переменные состояния
Вы хотите избежать дублирования в содержании состояния, чтобы отслеживать только существенное. Небольшое время, потраченное на переработку структуры состояния, сделает ваши компоненты более понятными, уменьшит дублирование и позволит избежать непреднамеренных значений. Ваша цель — предотвратить случаи, когда состояние в памяти не представляет ни одного допустимого пользовательского интерфейса, который вы хотели бы видеть. (Например, вы никогда не хотите одновременно отображать сообщение об ошибке и отключать ввод, иначе пользователь не сможет исправить ошибку!)
Вот некоторые вопросы, которые вы можете задать о своих переменных состояния:
-
Вызывает ли это состояние парадокс? Например,
isTypingиisSubmittingне могут быть одновременноtrue. Парадокс обычно означает, что состояние недостаточно ограничено. Существует четыре возможные комбинации двух булевых значений, но только три соответствуют допустимым состояниям. Чтобы удалить «невозможное» состояние, вы можете объединить их вstatusкоторое должно иметь одно из трех значений:'typing','submitting', или'success'. -
Такая же информация уже доступна в другой переменной состояния? Еще один парадокс:
isEmptyиisTypingне могут быть одновременноtrue. Делая их отдельными переменными состояния, вы рискуете тем, что они разойдутся и приведут к ошибкам. К счастью, вы можете удалитьisEmptyи вместо этого проверитьanswer.length === 0. -
Можно ли получить ту же информацию из обратного значения другой переменной состояния?
isErrorне нужно, потому что вы можете проверитьerror !== nullвместо этого.
После этой очистки у вас остается 3 (из 7!) необходимых переменных состояния:
const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);
const [status, setStatus] = useState('typing'); // 'typing', 'submitting', or 'success' Вы знаете, что они необходимы, потому что вы не можете удалить ни одну из них, не нарушив функциональность.
Глубокий анализ
Устранение «невозможных» состояний с помощью редьюсера
Эти три переменные — достаточно хорошее представление состояния этой формы. Однако всё ещё есть некоторые промежуточные состояния, которые не имеют полного смысла. Например, непустое error не имеет смысла, когда status равно 'success'. Чтобы более точно смоделировать состояние, можно вынести его в редьюсер. Редьюсеры позволяют объединять несколько переменных состояния в один объект и консолидировать всю связанную логику!
Шаг 5: Подключение обработчиков событий для установки состояния
Наконец, создайте обработчики событий, которые обновляют состояние. Ниже приведена окончательная форма со всеми подключенными обработчиками событий:
import { useState } from 'react'; export default function Form() { const [answer, setAnswer] = useState(''); const [error, setError] = useState(null); const [status, setStatus] = useState('typing'); if (status === 'success') { return <h1>That's right!</h1> } async function handleSubmit(e) { e.preventDefault(); setStatus('submitting'); try { await submitForm(answer); setStatus('success'); } catch (err) { setStatus('typing'); setError(err); } } function handleTextareaChange(e) { setAnswer(e.target.value); } return ( <> <h2>City quiz</h2> <p> In which city is there a billboard that turns air into drinkable water? </p> <form onSubmit={handleSubmit}> <textarea value={answer} onChange={handleTextareaChange} disabled={status === 'submitting'} /> <br /> <button disabled={ answer.length === 0 || status === 'submitting' }> Submit </button> {error !== null && <p className="Error"> {error.message} </p> } </form> </> ); } function submitForm(answer) { // Pretend it's hitting the network. return new Promise((resolve, reject) => { setTimeout(() => { let shouldError = answer.toLowerCase() !== 'lima' if (shouldError) { reject(new Error('Good guess but a wrong answer. Try again!')); } else { resolve(); } }, 1500); }); }
Хотя этот код длиннее, чем исходный императивный пример, он гораздо менее хрупок. Выражение всех взаимодействий как изменений состояния позволяет в дальнейшем вводить новые визуальные состояния без нарушения существующих. Это также позволяет изменять отображаемое содержимое в каждом состоянии без изменения логики взаимодействия.
Заключение
- Декларативное программирование означает описание пользовательского интерфейса для каждого визуального состояния, а не микроуправление им (императивное).
- При разработке компонента:
- Определите все его визуальные состояния.
- Определите человеческие и компьютерные триггеры для изменения состояния.
- Моделируйте состояние с помощью
useState. - Удалите неважные состояния, чтобы избежать ошибок и парадоксов.
- Подключите обработчики событий для установки состояния.
Попробуйте выполнить несколько заданий
Задача 1 из 3:
Добавление и удаление класса CSS
Сделайте так, чтобы щелчок по изображению удалял класс CSS background--active из внешнего <div>, но добавлял класс picture--active к <img>. Щелчок по фону снова должен восстановить исходные классы CSS.
Визуально вы должны ожидать, что щелчок по изображению удалит фиолетовый фон и выделит рамку изображения. Щелчок вне изображения выделит фон, но удалит выделение рамки изображения.
export default function Picture() { return ( <div className="background background--active"> <img className="picture" alt="Rainbow houses in Kampung Pelangi, Indonesia" src="https://i.imgur.com/5qwVYb1.jpeg" /> </div> ); }
© 2013–present Facebook Inc.
Licensed under the Creative Commons Attribution 4.0 International Public License.
https://18.react.dev/learn/reacting-to-input-with-state