Перейти к основному содержимому
Блокчейн43ГЛАВА 43Состояние и эффекты: интерфейс с памятьюamountinputbtnhintuseStateuseEffect → уборка

Состояние и эффекты: интерфейс с памятью

Прочитано0%
Квизы0/4
Тренажёрыне пройден0/1
Уровень 1 · Новичок

Зачем это нужно​

Компонент из прошлой главы — чистое вычисление: данные вошли, описание вышло. Такой интерфейс ничего не помнит.

Настоящий интерфейс помнит многое: что набрано в поле, какая вкладка открыта, идёт ли сейчас транзакция, что ответил узел. И умеет то, чего вычисление не умеет: делать что-то во внешнем мире — запрашивать баланс, подписываться на события контракта, ставить таймер.

Для первого есть состояние, для второго — эффекты. Обе темы просты в записи и полны ловушек в поведении, поэтому здесь всё разобрано живыми прогонами.

Состояние против обычной переменной​

function Counter() {
const [n, setN] = useState(0);
return <p onClick={() => setN(n + 1)}>{n}</p>;
}
Нажми на часть выражения

Прогон сравнивает состояние с обычной переменной, которая увеличивается в том же обработчике:

── состояние против обычной переменной ──
нажали дважды: на экране 2, обычная переменная 2
обычная тоже считает — но экран о ней не узнаёт, пока что-то не перерисуется

Обычная переменная считает верно. Разница в другом: изменение состояния заставляет компонент пересчитаться, а изменение обычной переменной — нет. На экране она появится случайно, когда пересчёт случится по другой причине.

Важно

Два признака того, что значение должно быть состоянием: его видно на экране и оно меняется во время работы. Всё остальное — обычные переменные и константы. Складывать в состояние то, что можно вычислить из других данных, — самая частая причина рассинхрона интерфейса.

Три вызова подряд​

── три setN(n + 1) подряд в одном обработчике ──
было 2, стало 3 — ожидали +3

── три setN(v => v + 1) подряд ──
было 3, стало 6
ТРИ ВЫЗОВА ПОДРЯД В ОДНОМ ОБРАБОТЧИКЕsetN(n + 1) × 3n — значение того рендера, где написанобработчик; по ходу оно не меняетсябыло 2 → стало 3ожидали +3, получили +1setN(v => v + 1) × 3функция получает актуальное значениена момент применения, а не при записибыло 3 → стало 6все три засчитаныновое значение зависит от старого — пишут функцию; не зависит — можно значениемто же правило спасает в таймерах и обработчиках ответа из сети: там значение устаревает почти всегда
Значение из рендера не меняется по ходу обработчика; функция получает актуальное на момент применения

Причина в том, что n внутри обработчика — значение того пересчёта, в котором обработчик был создан. Три раза подряд написано «поставь 3», и результат — 3.

Вторая форма передаёт функцию: «возьми то, что есть сейчас, и прибавь». Каждая из трёх применяется к результату предыдущей.

Правило простое: новое значение зависит от старого — пишите функцию. Не зависит (например, ставите значение из поля) — можно значением.

То же самое спасает в местах, где между созданием обработчика и его выполнением проходит время: таймеры, ответы из сети, подписки на события контракта. Там значение из пересчёта устаревает почти всегда.

Отвечено вопросов: 0 из 2
Три setN(n + 1) подряд увеличили счётчик на единицу. Почему?
Какое значение стоит держать в состоянии?
Отвечено верно: 0 из 2

Поле формы, привязанное к состоянию​

const [amount, setAmount] = useState("");

<input value={amount} onChange={(e) => setAmount(e.target.value)} />
<button disabled={amount === ""}>Отправить</button>
<p>в состоянии: "{amount}"</p>
── поле, привязанное к состоянию ──
кнопка выключена при пустом поле: true
набрали 2.5 → в состоянии "2.5", на экране в состоянии: "2.5"
кнопка теперь: включена
ПОЛЕ, ПРИВЯЗАННОЕ К СОСТОЯНИЮamount = «2.5»поле вводакнопка: включенаподсказка о суммеonChangeодно значение — и поле, и состояние кнопки, и подсказка: расходиться им негдедва поля, смотрящих в одно состояние, меняются одновременно — это проверено прогоном
Одно значение питает и поле, и состояние кнопки, и подсказку — расходиться им негде

Разница с ручным подходом принципиальная. Раньше значение жило в поле, и всё остальное — кнопка, подсказка, проверка — приходилось обновлять отдельно, вручную, не забывая ни одного места.

Здесь значение живёт в состоянии, а поле — лишь один из его читателей. Прогон это показал буквально: два поля, смотрящих в одно состояние, менялись одновременно.

Поле, в котором ничего не набирается​

Самая известная ловушка этого подхода:

── value есть, обработчика изменения нет ──
пытались набрать 999 → в поле осталось "5"
поле выглядит сломанным: буквы не появляются

Если задать полю значение из состояния, но не задать обработчик изменения, поле замирает. Пользователь жмёт клавиши — ничего не происходит.

Логика такая: значение поля полностью определяется состоянием. Нажатие клавиши состояние не меняет (обработчика нет) — значит, и поле остаётся прежним.

Три выхода:

  • обычный случай — добавить обработчик, как выше;
  • поле только для чтения — так и пометить: тогда понятно, что так задумано;
  • поле со свободным вводом — задать только начальное значение. Тогда поле живёт само, но и состояние про него ничего не знает.
── поле со свободным вводом ──
в поле: "что угодно", состояние про это ничего не знает

Где должно жить состояние​

Если одно значение нужно двум компонентам, оно живёт у их общего родителя, а вниз уходит свойствами. Это то самое «данные вниз, события вверх» из прошлой главы.

function Vault() {
const [amount, setAmount] = useState("");
return (
<>
<DepositForm amount={amount} onChange={setAmount} />
<Preview amount={amount} />
</>
);
}

Форма показывает значение и сообщает об изменениях; предпросмотр только читает. Ни один из них не владеет значением — владеет родитель.

Обратное правило тоже важно: если состояние нужно одному компоненту, оно живёт в нём. Поднимать всё наверх «на всякий случай» — верный способ получить один компонент на пятьсот строк, который пересчитывается от любого чиха.

Эффекты: выход во внешний мир​

Компонент обязан быть вычислением. Но интерфейсу нужно запрашивать баланс, подписываться на события контракта, ставить таймеры. Для этого есть отдельное место:

useEffect(() => {
let cancelled = false;
provider.getBalance(address).then((v) => {
if (!cancelled) setBalance(v);
});
return () => { cancelled = true; };
}, [address]);
Нажми на часть выражения

Порядок выполнения​

Прогон с тремя эффектами печатает, что и когда сработало:

── появление компонента ──
тело компонента, n = 0
эффект без списка зависимостей, n = 0
эффект с пустым списком — только при появлении
эффект со списком [n], n = 0

── состояние изменилось на 1 ──
тело компонента, n = 1
уборка за эффектом [n], n был 0
эффект без списка зависимостей, n = 1
эффект со списком [n], n = 1

── компонент убрали со страницы ──
уборка за эффектом [n], n был 1
ЧТО В КАКОМ ПОРЯДКЕ ВЫПОЛНЯЕТСЯпоявление компонентатело компонента, n = 0эффект [] — только при появленииэффект [n], n = 0состояние стало 1тело компонента, n = 1уборка за эффектом [n], n был 0эффект [n], n = 1компонент убрануборка за эффектом [n], n был 1эффект идёт ПОСЛЕтого, как экран обновлёнперед новым запуском —уборка за предыдущими один раз при уходе
Тело — потом уборка за прошлым эффектом — потом новый эффект; при уходе только уборка

Три наблюдения, каждое из которых потом экономит час отладки.

Тело компонента всегда первым. Эффект выполняется после того, как экран обновлён. Поэтому в эффекте можно измерить элемент — он уже на странице.

Эффект с пустым списком при изменении состояния не сработал. Только при появлении — как и обещано.

Уборка идёт перед новым запуском. В логе она стоит между телом и новым эффектом, и в ней видно старое значение: 0, когда новое уже 1. Так и должно быть — это уборка за предыдущим.

При уходе компонента — только уборка. Место, где снимают подписки. Не снять их — значит оставить работающий таймер или слушателя событий контракта, который пишет в состояние несуществующего компонента.

Интересный факт

В режиме разработки эффекты специально запускаются дважды — появление, уборка, появление. Это не поломка, а проверка: если эффект не переживает повторного запуска, значит, уборка написана неверно, и в настоящей работе это когда-нибудь всплывёт.

Отвечено вопросов: 0 из 2
В поле с value ничего не набирается. Что забыли?
Когда выполняется уборка за эффектом?
Отвечено верно: 0 из 2

Список зависимостей​

Три варианта, и путать их дорого:

записькогда выполняется
списка нетпосле каждого пересчёта
[]один раз при появлении
[a, b]при появлении и когда изменилось a или b

Главная ловушка — неполный список. Если внутри эффекта используется значение, которого нет в списке, эффект будет работать со старым: подпишется на прошлый адрес, запросит баланс не того кошелька. Ошибка молчаливая и очень неприятная.

Обратная ловушка — лишнее в списке. Объект или функция, созданные заново при каждом пересчёте, каждый раз «новые», и эффект запускается бесконечно.

Практическое правило: включайте в список всё, что читаете внутри эффекта. Если из-за этого эффект стал перезапускаться слишком часто — проблема не в списке, а в том, что значение пересоздаётся зря.

Типичные ошибки​

Изменение состояния прямо в теле компонента. Пересчёт → изменение → пересчёт → бесконечный цикл. Изменение состояния бывает только в обработчике или в эффекте.

Изменение массива или объекта на месте. Добавили элемент в существующий массив — ссылка та же, библиотека не видит изменения, экран не обновился. Создают новый: копия плюс новое.

setN(n + 1) несколько раз подряд. Разобрано выше.

Поле с value без обработчика. Замирает.

Запрос в сеть в теле компонента. Уйдёт при каждом пересчёте.

Неполный список зависимостей. Эффект работает со старыми значениями.

Забытая уборка. Подписка на события контракта остаётся жить после ухода компонента и пишет в никуда.

Состояние хранит вычислимое. Держат сумму и список, из которого она считается, — и однажды они расходятся.

Отвечено вопросов: 0 из 2
В списке зависимостей забыли значение, которое читается внутри эффекта. Что будет?
Добавили элемент в существующий массив состояния. Экран не обновился. Почему?
Отвечено верно: 0 из 2

Слова главы​

  • Состояние — значение, которое компонент помнит между пересчётами; его изменение вызывает пересчёт.
  • Функция изменения — не присваивание: текущая переменная в этом вызове не меняется.
  • Форма с функцией — set(v => …): берёт актуальное значение на момент применения.
  • Поле, привязанное к состоянию — значение из состояния плюс обработчик; без обработчика замирает.
  • Подъём состояния — общее значение живёт у общего родителя.
  • Эффект — код, выполняемый после отрисовки; место для выхода во внешний мир.
  • Уборка — снять подписку, таймер, отменить запрос; идёт перед новым запуском и при уходе.
  • Список зависимостей — что должно измениться, чтобы эффект повторился.

Тренажёр: набрать вслепую​

Цель: скорость ≥ 95 зн/мин, точность ≥ 88%

const [amount, setAmount] = useState("");

На чемпионате​

Состояние транзакции — обязательное. Не «отправляем» и «готово», а четыре положения: покой, подписываем, ждём подтверждения, готово или отказ. Пользователь должен видеть, на каком шаге всё встало.

Кнопка выключается на время операции. Одна строка: выключена, пока состояние не в покое. Иначе три нажатия — три транзакции.

Отписка в уборке — сразу, а не потом. Подписка на события контракта, оставленная после ухода компонента, — самый неприятный вид ошибки: он проявляется через десять минут работы и не воспроизводится по требованию.

Флаг отмены в каждом запросе. Сеть отвечает секунды, пользователь за это время успевает переключить вкладку или сменить адрес.

И общий совет: держите состояние как можно ниже. Одно большое хранилище на весь интерфейс кажется удобным ровно до того момента, когда любое нажатие начинает пересчитывать всё.

Куда углубиться​

  • Сложное состояние одним обработчиком — когда переходов много и они связаны.
  • Общее значение без передачи через все уровни — способ раздать тему или адрес кошелька вглубь.
  • Ссылка на значение без пересчёта — то, что надо помнить, но не показывать.
  • Свой набор действий — вынести работу с контрактом в отдельную функцию, чтобы переиспользовать.
  • Ссылка на элемент страницы — когда всё-таки нужен доступ к настоящему узлу.

Сводная проверка по всей главе: 8 вопросов, 5:00 на всё, без подсказок по ходу. Вопросы показываются по одному, ответ изменить нельзя. Если время выйдет — экзамен завершится с тем, что успел ответить. Пересдавать можно сколько угодно раз.

Куда дальше: проверенные ресурсы​

Видео по теме

Видео#15 React для НАЧИНАЮЩИХ (Хук useState)Какие-то уроки [Frontend]
Видео#16 React для НАЧИНАЮЩИХ (Хук useEffect)Какие-то уроки [Frontend]
ВидеоУправляемые и неуправляемые компоненты в React. Хук useRef и работа с DOMАлександр Ламков — Friendly Frontend
Видео10 - Контролируемые инпуты в React — useState и формыДюжев Алексей. с 0 в IT.
ВидеоУрок 1.15. Управляемые и неуправляемые компоненты в ReactWeb-garage

Комментарии

Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.