Состояние как моментальный снимок (Snapshot)
Переменные состояния могут выглядеть как обычные переменные JavaScript, которые можно читать и в которые можно записывать данные. Однако состояние ведет себя скорее как моментальный снимок. Его установка не изменяет уже имеющуюся переменную состояния, а вызывает повторную прорисовку.
Вы узнаете
- Как установка состояния запускает повторный рендеринг
- Когда и как обновляется состояние
- Почему состояние не обновляется сразу после его установки
- Как обработчики событий получают доступ к 'моментальному снимку' состояния
Установка состояния запускает рендеринг
Вы можете думать, что ваш пользовательский интерфейс меняется непосредственно в ответ на событие пользователя, такое как клик. В 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 обновляет экран в соответствии с этим снимком и подключает обработчики событий. В результате нажатие кнопки запустит обработчик события click из вашего JSX.
Когда React повторно рендерит компонент:
- React снова вызывает вашу функцию.
- Ваша функция возвращает новый снимок JSX.
- Затем React обновляет экран в соответствии со снимком, возвращенным вашей функцией.
Как память компонента, состояние не похоже на обычную переменную, которая исчезает после возврата вашей функции. Состояние фактически 'живет' в самом React — как будто на полке! — вне вашей функции. Когда React вызывает ваш компонент, он предоставляет вам снимок состояния для этого конкретного рендеринга. Ваш компонент возвращает снимок пользовательского интерфейса с новым набором пропсов и обработчиков событий в своем JSX, все рассчитанное с использованием значений состояния из этого рендеринга!
Вот небольшой эксперимент, чтобы показать, как это работает. В этом примере вы могли бы ожидать, что нажатие кнопки '+3' увеличит счетчик три раза, поскольку она вызывает 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>Вот что обработчик нажатия этой кнопки указывает React сделать:
setNumber(number + 1):numberравно0, поэтомуsetNumber(0 + 1).
- React готовится изменить
numberна1при следующем рендере.
setNumber(number + 1):numberравен0, поэтомуsetNumber(0 + 1).
- React готовится изменить
numberна1при следующем рендере.
setNumber(number + 1):numberравен0, поэтомуsetNumber(0 + 1).
- React готовится изменить
numberна1при следующем рендере.
Несмотря на то, что вы вызвали 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 при следующем нажатии и так далее.
Состояние во времени
Ну, это было весело. Попробуй угадать, что отобразится при нажатии этой кнопки:
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> </> )}Если использовать метод подстановки, о котором говорилось ранее, можно предположить, что в окне alert отобразится '0':
setNumber(0 + 5);
alert(0);Но что, если вы установите таймер на это предупреждение, чтобы оно срабатывало только после перерисовки компонента? Будет ли в нём отображаться '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.
setNumber(0 + 5);
setTimeout(() => {
alert(0);
}, 3000);Состояние, сохраненное в React, могло измениться к моменту запуска alert, но оно было запланировано с использованием моментального снимка состояния на момент взаимодействия пользователя с ним!
Значение переменной состояния никогда не меняется в пределах одного рендеринга, даже если код ее обработчика событий является асинхронным. Внутри onClick* этого рендеринга* значение number продолжает оставаться равным 0 даже после вызова setNumber(number + 5). Его значение было 'зафиксировано', когда React 'сделал снимок' пользовательского интерфейса, вызвав ваш компонент.
Вот пример того, как это делает ваши обработчики событий менее подверженными ошибкам синхронизации. Ниже приведена форма, которая отправляет сообщение с пятисекундной задержкой. Представьте себе следующий сценарий:
- Вы нажимаете кнопку 'Отправить', отправляя 'Hello' Алисе.
- До истечения пятисекундной задержки вы меняете значение поля 'Кому' на 'Боб'.
Что, по вашему мнению, отобразит alert? Отобразится ли 'Вы сказали Hello Алисе'? Или будет ли отображаться: 'Вы сказали Hello Бобу'? Сделайте предположение, основываясь на том, что вы знаете, а затем попробуйте:
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 сохраняет значения состояния 'фиксированными' в пределах обработчиков событий одного рендеринга. Вам не нужно беспокоиться о том, изменилось ли состояние во время выполнения кода.
Но что, если вы хотите прочитать последнее состояние перед повторным рендерингом? Вам понадобится использовать функцию обновления состояния, о которой пойдет речь на следующей странице!
Вывод
- Установка состояния вызывает новый рендеринг.
- React хранит состояние вне вашего компонента, как будто на полке.
- Когда вы вызываете
useState, React предоставляет вам снимок состояния для этого рендеринга. - Переменные и обработчики событий не 'переживают' повторные рендеринги. Каждый рендеринг имеет свои собственные обработчики событий.
- Каждый рендеринг (и функции внутри него) всегда будет 'видеть' тот снимок состояния, который React предоставил этому рендерингу.
- Вы можете мысленно подставлять состояние в обработчики событий, аналогично тому, как вы думаете о рендерируемом JSX.
- Обработчики событий, созданные ранее, имеют значения состояния из рендеринга, в котором они были созданы.
Рендеринг и фиксация
Прежде чем ваши компоненты появятся на экране, React должен их отрендерить. Понимание этапов этого процесса поможет вам проанализировать, как выполняется ваш код, и объяснить его поведение.
Последовательная обработка обновлений состояния
Установка значения переменной состояния запускает очередной рендеринг. Однако иногда может потребоваться выполнить несколько операций с этим значением, прежде чем запускать следующий рендеринг. Для этого полезно понять, как React группирует обновления состояния.