ChatGPT на русском языке, бесплатноНовости и обновления в Telegram
На sponsr есть решения ваших задач
Полезные видео о фронтенде. Подпишись на Rutube
Состояние как снимок в React
10 месяцев назад·5 мин. на чтение
Переменные состояния могут выглядеть как обычные переменные JavaScript, которые вы можете читать и записывать. Однако состояние в React больше похоже на снимок. Его установка не изменяет уже имеющуюся у вас переменную состояния, а вместо этого запускает повторный рендеринг.
Содержание туториала по React
Переменные состояния могут выглядеть как обычные переменные JavaScript, которые вы можете читать и записывать. Однако состояние больше похоже на снимок. Его установка не изменяет уже имеющуюся у вас переменную состояния, а вместо этого запускает повторный рендеринг.
Вот небольшой эксперимент, чтобы показать вам, как это работает. В этом примере вы можете ожидать, что нажатие кнопки «+3» увеличит счетчик на три, потому что он вызывается
Вот что обработчик нажатия этой кнопки сообщает React:
Вот пример того, как это делает ваши обработчики событий менее подверженными ошибкам синхронизации. Ниже представлена форма, которая отправляет сообщение с пятисекундной задержкой. Представьте себе этот сценарий:
Установка состояния запускает рендеринг
Вы можете думать о своем пользовательском интерфейсе как об изменении непосредственно в ответ на пользовательское событие, такое как клик. В React это работает немного иначе, чем эта ментальная модель. На предыдущей странице вы видели, что изменение состояния запрашивает повторный рендеринг. Это означает, что для того, чтобы интерфейс отреагировал на событие, вам необходимо обновить состояние. В этом примере, когда вы нажимаете "send",setIsSent(true)
сообщает React повторно отобразить пользовательский интерфейс:
Вот что происходит, когда вы нажимаете кнопку:import { useState } from 'react'; export default function Form() { const [isSent, setIsSent] = useState(false); const [message, setMessage] = useState('Hi!'); if (isSent) { return <h1>Your message is on its way!</h1>; } return ( <form onSubmit={(e) => { e.preventDefault(); setIsSent(true); sendMessage(message); }} > <textarea placeholder="Message" value={message} onChange={(e) => setMessage(e.target.value)} /> <button type="submit">Send</button> </form> ); } function sendMessage(message) { // ... }
- Выполняется обработчик события
onSubmit
. setIsSent(true)
устанавливаетisSent
вtrue
и кладет в очередь новый рендеринг.- React повторно отображает компонент в соответствии с новым значением
isSent
.
Рендеринг делает снимок во времени
"Рендеринг" означает, что React вызывает ваш компонент, который является функцией. JSX, который вы возвращаете из этой функции, подобен снимку пользовательского интерфейса в момент времени. Его пропсы, обработчики событий и локальные переменные были рассчитаны с использованием его состояния во время рендеринга. В отличие от фотографии или кадра фильма, "снимок" пользовательского интерфейса, который вы возвращаете, является интерактивным. Он включает в себя логику, такую как обработчики событий, которые определяют, что происходит в ответ на входные данные. Затем React обновляет экран в соответствии с этим снимком и подключает обработчики событий. В результате нажатие кнопки вызовет обработчик кликов из вашего JSX. Когда React перерисовывает компонент:- React снова вызывает вашу функцию.
- Ваша функция возвращает новый снимок JSX.
- Затем React обновляет экран в соответствии со снимком, который вы вернули.
setNumber(number + 1)
три раза.
Посмотрите, что происходит, когда вы нажимаете кнопку «+3»:
Обратите внимание, чтоimport { useState } from 'react'; export default function Counter() { const [number, setNumber] = useState(0); return ( <> <h1>{number}</h1> <button onClick={() => { setNumber(number + 1); setNumber(number + 1); setNumber(number + 1); }} > +3 </button> </> ); }
number
увеличивается только один раз за клик!
Установка состояния изменяет его только для следующего рендера. Во время первого рендера number
было 0
. Вот почему в обработчике этого рендеринга onClick
значение number
по-прежнему остается 0
даже после вызова setNumber(number + 1)
:
<button onClick={() => { setNumber(number + 1); setNumber(number + 1); setNumber(number + 1); }} > +3 </button>
setNumber(number + 1)
:number
равно0
таким образомsetNumber(0 + 1)
.- React готовится изменить
number
на1
в следующем рендеринге.
- React готовится изменить
setNumber(number + 1)
:number
равно0
таким образомsetNumber(0 + 1)
.- React готовится изменить
number
на1
в следующем рендеринге.
- React готовится изменить
setNumber(number + 1)
:number
равно0
таким образомsetNumber(0 + 1)
.- React готовится изменить
number
на1
в следующем рендеринге.
- React готовится изменить
setNumber(number + 1)
три раза, в этом рендере обработчик событий number
всегда равен 0
, поэтому вы устанавливаете состояние 1
три раза. Вот почему после завершения обработчика событий React повторно отображает компонент с number
равным 1
, а не 3
.
Вы также можете визуализировать это, мысленно заменяя переменные состояния их значениями в своем коде. Поскольку number
переменная состояния равна 0
для этого рендера, его обработчик событий выглядит так:
Для следующего рендера<button onClick={() => { setNumber(0 + 1); setNumber(0 + 1); setNumber(0 + 1); }} > +3 </button>
number
равен 1
, так что обработчик клика этого рендера выглядит так:
Вот почему повторное нажатие на кнопку установит счетчик на<button onClick={() => { setNumber(1 + 1); setNumber(1 + 1); setNumber(1 + 1); }} > +3 </button>
2
, затем 3
на следующий клик и т.д.
Состояние с течением времени
Попробуйте угадать, какое сообщение будет выведено вalert
при нажатии этой кнопки:
Если вы используете метод замены из предыдущего, вы можете догадаться, что предупреждение покажетimport { useState } from 'react'; export default function Counter() { const [number, setNumber] = useState(0); return ( <> <h1>{number}</h1> <button onClick={() => { setNumber(number + 5); alert(number); }} > +5 </button> </> ); }
0
:
Но что, если положитеsetNumber(0 + 5); alert(0);
alert
в таймер, чтобы оно срабатывало только после повторного рендеринга компонента? Будет ли он показывать 0
или 5
?
Если вы используете метод подстановки, вы можете увидеть «снимок» состояния, переданного вimport { useState } from 'react'; export default function Counter() { const [number, setNumber] = useState(0); return ( <> <h1>{number}</h1> <button onClick={() => { setNumber(number + 5); setTimeout(() => { alert(number); }, 3000); }} > +5 </button> </> ); }
alert
.
Состояние, хранящееся в React, могло измениться к моменту запуска оповещения, но оно было запланировано с использованием снимка состояния на момент взаимодействия с ним пользователя. Значение переменной состояния никогда не изменяется в процессе рендеринга, даже если код обработчика событий является асинхронным. ВнутриsetNumber(0 + 5); setTimeout(() => { alert(0); }, 3000);
onClick
этого рендера значение number
продолжает оставаться 0
даже после вызова setNumber(number + 5)
. Его значение было «фиксировано», когда React «сделал снимок» пользовательского интерфейса, вызвав ваш компонент.
- Вы нажимаете кнопку «Отправить», отправляя «Привет» Алисе.
- Прежде чем закончится пятисекундная задержка, вы измените значение поля «Кому» на «Боб».
alert
? Будет ли он отображать «Вы поздоровались с Алисой»? Или он будет отображать «Вы поздоровались с Бобом»? Сделайте предположение, основанное на том, что вы знаете:
React сохраняет значения состояния «фиксированными» в обработчиках событий одного рендеринга. Вам не нужно беспокоиться о том, изменилось ли состояние во время выполнения кода. Но что, если вы хотите прочитать последнее состояние перед повторным рендерингом? Вы захотите использовать функцию обновления состояния, описанную на следующей странице.import { useState } from 'react'; export default function Form() { const [to, setTo] = useState('Alice'); const [message, setMessage] = useState('Hello'); function handleSubmit(e) { e.preventDefault(); setTimeout(() => { alert(`You said ${message} to ${to}`); }, 5000); } return ( <form onSubmit={handleSubmit}> <label> To:{' '} <select value={to} onChange={(e) => setTo(e.target.value)}> <option value="Alice">Alice</option> <option value="Bob">Bob</option> </select> </label> <textarea placeholder="Message" value={message} onChange={(e) => setMessage(e.target.value)} /> <button type="submit">Send</button> </form> ); }
Резюме
- Состояние установки запрашивает новый рендеринг.
- React хранит состояние вне вашего компонента, как на полке.
- Когда вы вызываете
useState
, React дает вам снимок состояния для этого рендеринга . - Переменные и обработчики событий не «выживают» при повторном рендеринге. Каждый рендер имеет свои обработчики событий.
- Каждый рендер (и функции внутри него) всегда будет «видеть» снимок состояния, которое React дал этому рендеру.
- Вы можете мысленно заменить состояние в обработчиках событий, подобно тому, как вы думаете об отрендеренном JSX.
- Обработчики событий, созданные в прошлом, имеют значения состояния из рендеринга, в котором они были созданы.
Обработка ввода с состоянием в React
10 месяцев назад·8 мин. на чтение
React использует декларативный способ управления пользовательским интерфейсом. Вместо непосредственного управления отдельными частями пользовательского интерфейса вы описываете различные состояния, в которых может находиться ваш компонент, и переключаетесь между ними в ответ на действия пользователя. Это похоже на то, как дизайнеры думают о пользовательском интерфейсе.
Содержание туториала по React
В этой части руководства по React рассмотрим как реагировать на события ввода. React использует декларативный способ управления пользовательским интерфейсом. Вместо непосредственного управления отдельными частями пользовательского интерфейса вы описываете различные состояния, в которых может находиться ваш компонент, и переключаетесь между ними в ответ на действия пользователя. Это похоже на то, как дизайнеры думают о пользовательском интерфейсе.
Водитель не знают, куда вы хотите поехать, он просто следуют вашим командам. (И если вы ошибетесь, вы окажетесь не в том месте) Это называется императивным подходом, потому что вы должны «командовать» каждым элементом, от счетчика до кнопки, сообщая компьютеру, как обновить пользовательский интерфейс.
В этом примере императивного программирования пользовательского интерфейса форма создается без использования React. Он использует встроенный в браузер DOM:
Императивное управление пользовательским интерфейсом работает достаточно хорошо для отдельных примеров, но в более сложных системах управлять им становится экспоненциально сложнее. Представьте, что вы обновляете страницу, полную различных форм, подобных этой. Добавление нового элемента пользовательского интерфейса или нового взаимодействия потребует тщательной проверки всего существующего кода, чтобы убедиться, что вы не внесли ошибку (например, забыли что-то показать или скрыть).
React был создан, чтобы решить эту проблему.
В React вы не управляете пользовательским интерфейсом напрямую — это означает, что вы не включаете, не отключаете, не показываете и не скрываете компоненты напрямую. Вместо этого вы объявляете, что хотите показать, а React выясняет, как обновить пользовательский интерфейс. Подумайте о том, чтобы сесть в такси и сказать водителю, куда вы хотите ехать, вместо того, чтобы указывать ему, где именно повернуть. Доставить вас туда — работа водителя, и они могут даже знать некоторые короткие пути, о которых вы не подумали.
Шаг 3: Представить состояние в памяти, используя
Далее вам нужно представить визуальные состояния вашего компонента в памяти с помощью
Сравнение декларативного пользовательского интерфейса с императивным
Когда вы проектируете взаимодействия с пользовательским интерфейсом, вы, вероятно, думаете о том, как пользовательский интерфейс изменяется в ответ на действия пользователя. Рассмотрим форму, которая позволяет пользователю отправить ответ:- Когда вы вводите что-то в форму, кнопка «Отправить» становится активной.
- Когда вы нажимаете «Отправить», и форма, и кнопка блокируются, и появляется анимация ожидания.
- Если сетевой запрос выполнен успешно, форма скрывается и появляется сообщение «Спасибо».
- Если сетевой запрос завершается неудачно, появляется сообщение об ошибке, и форма снова становится доступной.
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;
<!-- index.html --> <form id="form"> <h2>City quiz</h2> <p>What city is located on two continents?</p> <textarea id="textarea"></textarea> <br /> <button id="button" disabled>Submit</button> <p id="loading" style="display: none">Loading...</p> <p id="error" style="display: none; color: red;"></p> </form> <h1 id="success" style="display: none">That's right!</h1> <style> * { box-sizing: border-box; } body { font-family: sans-serif; margin: 20px; padding: 0; } </style>
Декларативный подход к пользовательскому интерфейсу
Вы видели, как императивно реализовать форму выше. Чтобы лучше понять, как мыслить в React, выполним реализацию этого пользовательского интерфейса в React:- Определить различные визуальные состояния компонента
- Определить, что вызывает эти изменения состояния
- Представить состояние в памяти, используя
useState
- Удалить все несущественные переменные состояния
- Подключить обработчики событий для установки состояния
Шаг 1. Определить различные визуальные состояния компонента
В информатике вы могли слышать о «машине состояний» (state machine, стейт машина, конечный автомат), находящейся в одном из нескольких «состояний». Если вы работаете с дизайнером, возможно, вы видели мокапы для разных «визуальных состояний». React стоит на стыке дизайна и информатики, поэтому обе эти идеи являются источниками вдохновения. Во-первых, вам нужно визуализировать все различные «состояния» пользовательского интерфейса, которые может видеть пользователь:- Пусто (
empty
): в форме отключена кнопка «Отправить». - Ввод (
typing
): Форма имеет активную кнопку «Отправить». - Отправка (
submitting
): Форма полностью отключена. Показана анимация ожидания. - Успех (
success
): вместо формы отображается сообщение «Спасибо». - Ошибка (
error
): то же, что и состояние ввода, но с дополнительным сообщением об ошибке.
status
, со значением по умолчанию 'empty'
:
Как отобразить множество визуальных состояний компонента одновременно? Если у компонента много визуальных состояний, как у компонента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> </> ); }
Form
выше, будет удобно показать их все на одной странице.
Такие страницы часто называют «живыми руководствами по стилю» (living styleguides) или storybooks.
Шаг 2. Определить, что вызывает эти изменения состояния
Вы можете запускать обновления состояния в ответ на два типа входных данных:- Ввод пользователя, например нажатие кнопки, ввод в поле, переход по ссылке.
- Входные данные компьютера, такие как получение сетевого ответа, завершение тайм-аута, загрузка изображения.
- Изменение ввода текста (пользователем) должно переключить его из пустого (empty) состояния в состояние ввода (typing) или обратно, в зависимости от того, пусто текстовое поле или нет.
- Нажатие (пользователем) кнопки «Отправить» должно переключить ее в состояние «Отправка» (submitting).
- Успешный сетевой ответ (компьютер) должен перевести его в состояние успеха (success).
- Неудачный сетевой ответ (компьютер) должен перевести его в состояние (error) с соответствующим сообщением об ошибке.
Шаг 3: Представить состояние в памяти, используя useState
Далее вам нужно представить визуальные состояния вашего компонента в памяти с помощью useState
. Ключевым моментом является простота: каждая часть состояния является «движущейся частью», и вам нужно как можно меньше «движущихся частей». Большая сложность приводит к большему количеству ошибок.
Начните с состояния, которое обязательно должно быть. Например, вам нужно сохранить ответ для ввода и ошибку (если она существует) для сохранения последней ошибки:
Затем вам понадобится переменная состояния, представляющая, какое из визуальных состояний, описанных ранее, вы хотите отобразить. Обычно существует более одного способа представить это в памяти, поэтому вам нужно поэкспериментировать с ним. Если вы изо всех сил пытаетесь сразу придумать лучший способ, начните с добавления достаточного количества состояний, чтобы быть уверенным, что охватываются все возможные визуальные состояния: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
не могут одновременно быть истинными. Парадокс обычно означает, что состояние недостаточно ограничено. Существует четыре возможных комбинации двух логических значений, но только три соответствуют действительным состояниям. Чтобы удалить невозможное состояние, вы можете объединить их в состояние, которое должно иметь одно из трех значений: «ввод», «отправка» или «успех». - Доступна ли та же информация в другой переменной состояния? Еще один парадокс:
isEmpty
иisTyping
не могут быть истинными одновременно. Делая их отдельными переменными состояния, вы рискуете рассинхронизировать их и вызвать ошибки. К счастью, вы можете удалитьisEmpty
и вместо этого проверитьanswer.length === 0
. - Можно ли получить ту же информацию из инверсии другой переменной состояния?
isError
не нужен, потому что вместо этого вы можете проверитьerror !== null
.
Вы знаете, что они необходимы, потому что вы не можете удалить ни один из них, не нарушив функциональность.const [answer, setAnswer] = useState(''); const [error, setError] = useState(null); const [status, setStatus] = useState('typing'); // 'typing', 'submitting', or '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
. - Удалите несущественное состояние, чтобы избежать ошибок и парадоксов.
- Подключите обработчики событий для установки состояния.