ChatGPT на русском языке, бесплатноНовости и обновления в Telegram
На sponsr есть решения ваших задач
Полезные видео о фронтенде. Подпишись на Rutube
Обработка ввода с состоянием в 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
. - Удалите несущественное состояние, чтобы избежать ошибок и парадоксов.
- Подключите обработчики событий для установки состояния.
Доступ к данным через рефы в React
10 месяцев назад·6 мин. на чтение
Если вы хотите, чтобы React компонент «запоминал» некоторую информацию, но вы не хотите, чтобы эта информация запускала новые рендеры, вы можете использовать ref (рефы)
Содержание туториала по React
Если вы хотите, чтобы компонент «запоминал» некоторую информацию, но вы не хотите, чтобы эта информация запускала новые рендеры, вы можете использовать ref (рефы).
При нажатии кнопки «Стоп» нужно отменить существующий интервал, чтобы он перестал обновлять переменную состояния now. Вы можете сделать это, вызвав
Вот кнопка-счетчик, реализованная с состоянием:
Добавление рефа в компонент
Вы можете добавить реф в свой компонент, импортировав хукuseRef
из React:
Внутри вашего компонента вызовите хукimport { useRef } from 'react';
useRef
и передайте начальное значение, на которое вы хотите сослаться, в качестве единственного аргумента. Например, вот ссылка на значение 0
:
const ref = useRef(0);
useRef
возвращает такой объект:
Вы можете получить доступ к текущему значению этого рефа через свойство{ current: 0; // Значение, отправленное в useRef }
ref.current
. Это значение является изменяемым, то есть вы можете как читать, так и писать в него. Это как секретный карман вашего компонента, который React не отслеживает. (Вот что делает его лозейкой из одностороннего потока данных React — подробнее об этом ниже)
В примере ниже кнопка будет увеличивать ref.current
при каждом нажатии:
Реф указывает на число, но, как и состояние, вы можете указать что угодно: строку, объект или даже функцию. В отличие от состояния,import { useRef } from 'react'; export default function Counter() { let ref = useRef(0); function handleClick() { ref.current = ref.current + 1; alert('You clicked ' + ref.current + ' times!'); } return <button onClick={handleClick}>Click me!</button>; }
ref
— это простой объект JavaScript со свойством current
, которое вы можете читать и изменять.
Обратите внимание, что компонент не перерисовывается при очередном инкременте. Как и состояние, React сохраняет рефы между повторными рендерингами. Однако установка состояния повторно рендерит компонент. А изменение рефа - нет.
Пример: создание секундомера
Вы можете комбинировать рефы и состояние в одном компоненте. Например, давайте создадим секундомер, который пользователь может запускать или останавливать нажатием кнопки. Чтобы отобразить, сколько времени прошло с тех пор, как пользователь нажал «Старт», вам нужно будет отслеживать, когда была нажата кнопка «Старт» и какое сейчас время. Эта информация используется для рендеринга, поэтому вы сохраните ее в состоянии:Когда пользователь нажимает «Старт», вы будете использоватьconst [startTime, setStartTime] = useState(null); const [now, setNow] = useState(null);
setInterval
для обновления времени каждые 10 миллисекунд:
import { useState } from 'react'; export default function Stopwatch() { const [startTime, setStartTime] = useState(null); const [now, setNow] = useState(null); function handleStart() { // Начало отсчета setStartTime(Date.now()); setNow(Date.now()); setInterval(() => { // Обновление текущего времени каждые 10 мс setNow(Date.now()); }, 10); } let secondsPassed = 0; if (startTime != null && now != null) { secondsPassed = (now - startTime) / 1000; } return ( <> <h1>Time passed: {secondsPassed.toFixed(3)}</h1> <button onClick={handleStart}>Start</button> </> ); }
clearInterval
, но вам нужно указать идентификатор интервала, который ранее возвращался вызовом setInterval
, когда пользователь нажимал «Старт». Вам нужно где-то хранить идентификатор интервала. Поскольку идентификатор интервала не используется для рендеринга, вы можете сохранить его в рефе:
Когда информация используется для рендеринга - держите ее в состоянии. Когда информации нужна только обработчикам событий и ее изменение не требует повторного рендеринга - использование ссылки может быть более эффективным.import { useState, useRef } from 'react'; export default function Stopwatch() { const [startTime, setStartTime] = useState(null); const [now, setNow] = useState(null); const intervalRef = useRef(null); function handleStart() { setStartTime(Date.now()); setNow(Date.now()); clearInterval(intervalRef.current); intervalRef.current = setInterval(() => { setNow(Date.now()); }, 10); } function handleStop() { clearInterval(intervalRef.current); } let secondsPassed = 0; if (startTime != null && now != null) { secondsPassed = (now - startTime) / 1000; } return ( <> <h1>Time passed: {secondsPassed.toFixed(3)}</h1> <button onClick={handleStart}>Start</button> <button onClick={handleStop}>Stop</button> </> ); }
Различия между рефами и состоянием
Возможно, вы думаете, что рефы кажутся менее «строгими», чем состояние. Например, вы можете изменить их вместо того, чтобы постоянно использовать функцию установки состояния. Но в большинстве случаев вы захотите использовать состояние. Рефы — это лозейка, которая вам часто не потреюуется. Сравним состояние и рефы:Рефы | Состояние |
---|---|
useRef(initialValue) возвращает { current: initialValue } | useState(initialValue) возвращает текущее значение переменной состояния и функцию установки состояния ([value, setValue] ) |
Не запускает повторный рендеринг при его изменении. | Запускает повторный рендеринг при его изменении. |
Изменяемый — вы можете изменять и обновлять значение current вне процесса рендеринга. | Неизменный (иммутабельный) — вы должны использовать функцию установки состояния, чтобы изменить переменные состояния, чтобы поставить в очередь повторный рендеринг. |
Вы не должны читать (или записывать) значение current во время рендеринга. | Вы можете прочитать состояние в любое время. Однако каждый рендер имеет свой собственный снимок состояния, который не меняется. |
Поскольку отображается значениеimport { useState } from 'react'; export default function Counter() { const [count, setCount] = useState(0); function handleClick() { setCount(count + 1); } return <button onClick={handleClick}>You clicked {count} times</button>; }
count
, имеет смысл использовать для него значение состояния. Когда значение счетчика устанавливается с помощью setCount()
, React повторно отображает компонент, и экран обновляется, чтобы отразить новое значение счетчика.
Если вы попытаетесь реализовать это с помощью ref
, React никогда не будет повторно отображать компонент, поэтому вы никогда не увидите изменения счетчика. Посмотрите, как нажатие на эту кнопку не обновляет ее текст:
Вот почему чтениеimport { useRef } from 'react'; export default function Counter() { let countRef = useRef(0); function handleClick() { // В таком случае ререндеринг компонента Не происходит countRef.current = countRef.current + 1; } return ( <button onClick={handleClick}>You clicked {countRef.current} times</button> ); }
ref.current
во время рендеринга приводит к ненадежному коду. Если вам это нужно, используйте вместо этого состояние.
Как работает useRef внутри?
Хотя иuseState
, и useRef
предоставляются React, в принципе useRef
может быть реализован поверх useState
. Вы можете представить, что внутри React useRef
реализован так:
Во время первого рендеринга// Внутри React function useRef(initialValue) { const [ref, unused] = useState({ current: initialValue }); return ref; }
useRef
возвращает { current: initialValue }
. Этот объект хранится в React, поэтому при следующем рендеринге будет возвращен тот же объект. Обратите внимание, что в этом примере не используется установщик состояния. В этом нет необходимости, потому что useRef
всегда должен возвращать один и тот же объект.
React предоставляет встроенную версию useRef
, потому что она достаточно распространена на практике. Но вы можете думать об этом как об обычной переменной состояния без сеттера. Если вы знакомы с объектно-ориентированным программированием, рефы могут напомнить вам поля экземпляра, но вместо this.something
вы пишете somethingRef.current
.
Когда использовать рефы?
Как правило, вы будете использовать рефы, когда вашему компоненту нужно «выйти за пределы» React и взаимодействовать с внешними API — часто API браузера, который не повлияет на внешний вид компонента. Вот несколько таких редких ситуаций:- Хранение идентификаторов таймаута
- Хранение элементов DOM и управление ими, которые мы рассмотрим в следующей части.
- Хранение других объектов, которые не нужны для вычисления JSX.
Рекомендации по использованию рефов
Следование этим принципам сделает ваши компоненты более предсказуемыми:- Относитесь к рефам как к лозейке. Рефы полезны при работе с внешними системами или API-интерфейсами браузера. Если большая часть логики вашего приложения и потока данных зависит от рефов, возможно, следует пересмотреть свой подход.
- Не читайте и не записывайте
ref.current
во время рендеринга. Если во время рендеринга требуется некоторая информация, используйте вместо этого состояние. Поскольку React не знает, когда изменяетсяref.current
, даже его чтение во время рендеринга затрудняет прогнозирование поведения вашего компонента. (Единственным исключением является такой код, как if(!ref.current) ref.current = new Thing()
, который устанавливает ссылку только один раз во время первого рендеринга.)
ref
, оно немедленно меняется:
Это связано с тем, что сам реф является обычным JavaScript объектом и ведет себя как объект. Вам также не нужно беспокоиться о том, чтобы избежать мутации, когда вы работаете с рефами. Пока объект, который вы мутируете, не используется для рендеринга, React не волнует, что вы делаете с рефом или его содержимым.ref.current = 5; console.log(ref.current); // 5
Рефы и DOM
Вы можете указать ссылку на любое значение. Однако наиболее распространенным вариантом использования ссылки является доступ к элементу DOM. Например, это удобно, если вы хотите сфокусировать инпут программно. Когда вы передаете реф в атрибутref
в JSX, например <div ref={myRef}>
, React поместит соответствующий элемент DOM в myRef.current
.
Резюме
- Рефы — это лозейка для хранения значений, которые не используются для рендеринга. Вы не будете нуждаться в них часто.
- Ссылка — это простой объект JavaScript с одним свойством, называемым
current
, которое вы можете прочитать или установить. - Вы можете попросить React дать вам реф, вызвав хук
useRef
. - Как и состояние, рефы позволяют сохранять информацию между повторными рендерингами компонента.
- В отличие от состояния, установка значения
current
рефа не вызывает повторный рендеринг. - Не читайте и не записывайте
ref.current
во время рендеринга. Это затрудняет прогнозирование вашего компонента.