Реагирование на ввод с помощью состояний
React предоставляет декларативный подход к управлению пользовательским интерфейсом. Вместо того чтобы напрямую манипулировать отдельными элементами интерфейса, вы описываете различные состояния, в которых может находиться ваш компонент, и переключаетесь между ними в ответ на действия пользователя. Это похоже на то, как дизайнеры подходят к созданию пользовательского интерфейса.
Вы узнаете
- Чем декларативное программирование пользовательского интерфейса отличается от императивного
- Как перечислить различные визуальные состояния, в которых может находиться ваш компонент
- Как инициировать переход между различными визуальными состояниями с помощью кода
Сравнение декларативного и императивного программирования пользовательского интерфейса
Когда вы проектируете взаимодействия пользовательского интерфейса, вы, вероятно, думаете о том, как интерфейс изменяется в ответ на действия пользователя. Рассмотрим форму, позволяющую пользователю отправить ответ:
- Когда вы вводите что-либо в форму, кнопка 'Отправить' становится активной.
- Когда вы нажимаете 'Отправить', и форма, и кнопка становятся неактивными, и появляется индикатор загрузки.
- Если сетевой запрос прошел успешно, форма скрывается, и **появляется сообщение 'Спасибо'. **
- Если сетевой запрос завершился неудачей, появляется сообщение об ошибке, и форма снова становится активной.
В императивном программировании вышеописанное напрямую соответствует тому, как вы реализуете взаимодействие. Вам приходится писать точные инструкции для управления пользовательским интерфейсом в зависимости от того, что только что произошло. Вот еще один способ посмотреть на это: представьте, что вы едете в машине рядом с кем-то и пошагово говорите ему, куда ехать.
Он не знает, куда вы хотите поехать, он просто следует вашим командам. (И если вы ошибетесь в указаниях, то окажетесь не там, где нужно! ) Это называется императивным, потому что вам приходится 'командовать' каждым элементом, от индикатора загрузки до кнопки, указывая компьютеру, как обновлять пользовательский интерфейс.
В этом примере императивного программирования пользовательского интерфейса форма построена без React. В ней используется только браузерный DOM:
//index.js
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) {
// Предположим, что происходит подключение к сети.
return new Promise((resolve, reject) => {
setTimeout(() => {
if (answer.toLowerCase() === 'istanbul') {
resolve();
} else {
reject(new Error('Хорошее предположение, но ответ неверный. Попробуй еще раз!'));
}
}, 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> </> );}Отображение нескольких визуальных состояний одновременно
Если компонент имеет много визуальных состояний, может быть удобно отобразить их все на одной странице:
//App.js
import Form from './Form.js';
let statuses = [
'empty',
'typing',
'submitting',
'success',
'error',
];
export default function App() {
return (
<>
{statuses.map(status => (
<section key={status}>
<h4>Форма ({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' или '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. - Удалите несущественные состояния, чтобы избежать ошибок и парадоксов.
- Подключите обработчики событий для установки состояния.
Управление состоянием
По мере роста вашего приложения становится все важнее продуманно подходить к организации состояния и потокам данных между компонентами. Избыточное или дублирующееся состояние — частая причина ошибок. В этой главе вы узнаете, как правильно структурировать состояние, как обеспечить удобство обслуживания логики обновления состояния и как совместно использовать состояние между удаленными компонентами.
Выбор структуры состояния
Правильная организация состояния может стать решающим фактором, определяющим, будет ли компонент удобен для модификации и отладки или станет постоянным источником ошибок. Вот несколько советов, которые стоит учитывать при организации состояния.