Последовательная обработка обновлений состояния
Установка значения переменной состояния запускает очередной рендеринг. Однако иногда может потребоваться выполнить несколько операций с этим значением, прежде чем запускать следующий рендеринг. Для этого полезно понять, как React группирует обновления состояния.
Вы узнаете
- Что такое «пакетирование» и как React использует его для обработки нескольких обновлений состояния
- Как применить несколько обновлений к одной и той же переменной состояния подряд
React группирует обновления состояния
Вы можете ожидать, что нажатие кнопки «+3» увеличит счетчик на три единицы, поскольку она вызывает setNumber(number + 1) три раза:
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> </> )}Однако, как вы, возможно, помните из предыдущего раздела, [значения состояния при каждом рендере фиксированы](/learn/state-as-a-snapshot# rendering-takes-a-snapshot-in-time), поэтому значение number внутри обработчика событий первого рендеринга всегда равно 0, независимо от того, сколько раз вы вызываете setNumber(1):
setNumber(0 + 1);
setNumber(0 + 1);
setNumber(0 + 1);Но здесь есть ещё один фактор. React ждёт, пока не будет выполнен весь код в обработчиках событий, прежде чем обрабатывать обновления состояния. Именно поэтому повторный рендеринг происходит только после всех этих вызовов setNumber().
Это может напомнить вам официанта, принимающего заказ в ресторане. Официант не бежит на кухню, как только вы назвали первое блюдо! Вместо этого он дает вам закончить заказ, позволяет внести в него изменения и даже принимает заказы от других людей за столом.
Это позволяет обновлять несколько переменных состояния — даже из нескольких компонентов — без вызова слишком большого количества повторных рендеров. Но это также означает, что пользовательский интерфейс не будет обновлен до тех пор, пока не завершится работа вашего обработчика событий и любого кода в нём. Такое поведение, также известное как пакетная обработка, делает работу вашего приложения React намного быстрее.
Кроме того, это позволяет избежать запутанных «незавершенных» рендеров, при которых обновляются только некоторые переменные.
React не объединяет в пакеты несколько намеренных событий, таких как клики — каждый клик обрабатывается отдельно. Будьте уверены, что React выполняет пакетную обработку только в тех случаях, когда это в целом безопасно. Это гарантирует, что, например, если первый клик по кнопке отключает форму, второй клик не отправит её снова.
Обновление одного и того же состояния несколько раз перед следующим рендерингом
Это редкий случай использования, но если вы хотите обновить одну и ту же переменную состояния несколько раз перед следующим рендерингом, вместо передачи следующего значения состояния, как setNumber(number + 1), вы можете передать функцию, которая вычисляет следующее состояние на основе предыдущего в очереди, как setNumber(n => n + 1). Это способ сказать React «сделать что-то со значением состояния», а не просто заменить его.
Попробуйте теперь увеличить счетчик:
import { useState } from 'react';export default function Counter() { const [number, setNumber] = useState(0); return ( <> <h1>{number}</h1> <button onClick={() => { setNumber(n => n + 1); setNumber(n => n + 1); setNumber(n => n + 1); }}>+3</button> </> )}Здесь n => n + 1 называется функцией обновления. Когда вы передаете её в сеттер состояния:
- React ставит эту функцию в очередь для обработки после того, как будет выполнен весь остальной код в обработчике события.
- Во время следующего рендеринга React проходит по очереди и предоставляет вам окончательное обновлённое состояние.
setNumber(n => n + 1);
setNumber (n => n + 1);
setNumber(n => n + 1);Вот как React обрабатывает эти строки кода при выполнении обработчика событий:
setNumber(n => n + 1):n => n + 1— это функция. React добавляет её в очередь.setNumber(n => n + 1):n => n + 1— это функция. React добавляет её в очередь.setNumber(n => n + 1):n => n + 1— это функция. React добавляет её в очередь.
Когда вы вызываете useState во время следующего рендеринга, React проходит по очереди. Предыдущее значение состояния number было 0, поэтому именно это React передает в первую функцию обновления в качестве аргумента n. Затем React берет возвращаемое значение вашей предыдущей функции обновления и передает его следующей функции обновления в качестве n, и так далее:
| обновление в очереди | n | возвращает |
|---|---|---|
n => n + 1 | 0 | 0 + 1 = 1 |
n => n + 1 | 1 | 1 + 1 = 2 |
n => n + 1 | 2 | 2 + 1 = 3 |
React сохраняет 3 в качестве конечного результата и возвращает его из useState.
Именно поэтому нажатие кнопки «+3» в приведенном выше примере правильно увеличивает значение на 3.
Что произойдет, если обновить состояние после его замены
А как насчет этого обработчика событий? Как вы думаете, каким будет значение number при следующем рендере?
<button onClick={() => {
setNumber(number + 5);
setNumber(n => n + 1);
}}>import { useState } from 'react';export default function Counter() { const [number, setNumber] = useState(0); return ( <> <h1>{number}</h1> <button onClick={() => { setNumber(number + 5); setNumber(n => n + 1); }}>Increase the number</button> </> )}Вот что этот обработчик событий указывает React:
setNumber(number + 5):numberравно0, поэтомуsetNumber(0 + 5). React добавляет «заменить на5» в свою очередь.setNumber(n => n + 1):n => n + 1— это функция обновления. React добавляет эту функцию в свою очередь.
Во время следующего рендеринга React проходит по очереди состояний:
| обновление в очереди | n | возвращает |
|---|---|---|
«заменить на 5» | 0 (не используется) | 5 |
n => n + 1 | 5 | 5 + 1 = 6 |
React сохраняет 6 в качестве конечного результата и возвращает его из useState.
Вы, возможно, заметили, что setState(5) на самом деле работает как setState (n => 5), но n не используется!
Что произойдет, если заменить состояние после его обновления
Давайте попробуем ещё один пример. Как вы думаете, каким будет значение number при следующем рендере?
<button onClick={() => {
setNumber(number + 5);
setNumber(n => n + 1);
setNumber(42);
}}>import { useState } from 'react';export default function Counter() { const [number, setNumber] = useState(0); return ( <> <h1>{number}</h1> <button onClick={() => { setNumber(number + 5); setNumber(n => n + 1); setNumber(42); }}>Increase the number</button> </> )}Вот как React обрабатывает эти строки кода при выполнении этого обработчика событий:
setNumber(number + 5):numberравно0, поэтомуsetNumber(0 + 5). React добавляет «заменить на5» в свою очередь.setNumber(n => n + 1):n => n + 1— это функция обновления. React добавляет эту функцию в свою очередь.setNumber(42): React добавляет «заменить на42» в свою очередь.
Во время следующего рендеринга React проходит по очереди состояний:
| обновление в очереди | n | возвращает |
|---|---|---|
«заменить на 5» | 0 (не используется) | 5 |
n => n + 1 | 5 | 5 + 1 = 6 |
«заменить на 42» | 6 (не используется) | 42 |
Затем React сохраняет 42 в качестве конечного результата и возвращает его из useState.
Подводя итог, вот как можно представить себе то, что вы передаете в сеттер состояния setNumber:
- Функция обновления (например,
n => n + 1) добавляется в очередь. - Любое другое значение (например, число
5) добавляет в очередь «заменить на5», игнорируя то, что уже находится в очереди.
После завершения работы обработчика события React запустит повторный рендеринг. Во время повторного рендеринга React обработает очередь. Функции обновления запускаются во время рендеринга, поэтому функции обновления должны быть чистыми и только возвращать результат. Не пытайтесь устанавливать состояние изнутри них или запускать другие побочные эффекты. В строгом режиме React запустит каждую функцию обновления дважды (но отбросит второй результат), чтобы помочь вам найти ошибки.
Конвенции именования
Обычно аргументы функций обновления называют по первым буквам соответствующих переменных состояния:
setEnabled(e => !e);
setLastName(ln => ln.reverse());
setFriendCount(fc => fc * 2);Если вы предпочитаете более развернутый код, другой распространённой конвенцией является повторение полного имени переменной состояния, например setEnabled(enabled => !enabled), или использование префикса, например setEnabled(prevEnabled => !prevEnabled).
Вывод
- Установка состояния не изменяет переменную в существующем рендере, но запрашивает новый рендер.
- React обрабатывает обновления состояния после того, как обработчики событий завершили работу. Это называется пакетной обработкой.
- Чтобы обновить какое-либо состояние несколько раз в одном событии, можно использовать функцию обновления
setNumber(n => n + 1).
Состояние как моментальный снимок (Snapshot)
Переменные состояния могут выглядеть как обычные переменные JavaScript, которые можно читать и в которые можно записывать данные. Однако состояние ведет себя скорее как моментальный снимок. Его установка не изменяет уже имеющуюся переменную состояния, а вызывает повторную прорисовку.
Обновление объектов в state
State может содержать любые значения JavaScript, включая объекты. Однако не следует напрямую изменять объекты, хранящиеся в state React. Вместо этого, когда вам нужно обновить объект, необходимо создать новый (или сделать копию существующего), а затем установить state так, чтобы он использовал эту копию.