Постановка в очередь серии обновления состояний
Обновление переменной состояния запускает в очередь новый рендер. Но иногда вам может понадобиться выполнить множество операций со значением перед следующим рендером. Чтобы сделать это, стоит понять, как React группирует обновления состояния.
You will learn
- Что такое “группировка” и как React иcпользует ее для обработки множества обновлений состояния
- Как назначить несколько обновлений к одной и той же переменной состояния подряд
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> </> ) }
Кроме того, как вы, возможно, помните из прошлой секции, each render’s state values are fixed, значение number
внутри обработчика событий первого рендера всегда равно 0
, независимо от того, сколько раз вы вызвали функцию setNumber(1)
:
setNumber(0 + 1);
setNumber(0 + 1);
setNumber(0 + 1);
Кроме этого, есть еще один важный фактор. React будет ждать пока весь код во всех обработчиках событий отработает, перед тем как выполнить обновления состояния. Вот почему ре-рендер происходит только после всех вызовов setNumber()
.
Для примера воспомним официанта, который принимает заказ в ресторане. Официант не бежит на кухню сразу после того, как услышал первое блюдо! Вместо этого, он ждет пока вы закончите свой заказ, уточняет его детали, и даже принимает заказы от других людей за столом.
Illustrated by Rachel Lee Nabors
Это позволяет нам обновлять несколько переменных состояния—даже от нескольких компонентов—без вызова слишком большком большого количества ре-рендеров. Но это также означает, что UI не будет обновлен до того, как ваши обработчики событий, и код в них, не исполнится. Это поведение, также известное как группировка, позволяет вашему 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
, так что эта функция обновления и пропускает в следующем обновление как 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); }}>Увеличить число</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
.
Что случится, если вы замените состояние после его обновления
Давайте посмотрим еще один пример. Как вы думаете, какое значение примет 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); }}>Увеличить число</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 обработает очередь. Обновляющие функции выполняются во время рендеринга, так что так что они должны быть чистыми и возвращать только результат. Не пытайтесь устанавливать состояние изнутри или запускать другие побочные эффекты.
Соглашения об именах
Достаточно часто аргумент обновляющей функции обозначают первыми буквами связанной с ней переменной состояния:
setEnabled(e => !e);
setLastName(ln => ln.reverse());
setFriendCount(fc => fc * 2);
Если вы предпочитаете более многословный вариант, другим распространенным соглашенением является повторение полного наименования переменной состояния, типа setEnabled(enabled => !enabled)
, или же использование префикса типа setEnabled(prevEnabled => !prevEnabled)
.
Recap
- Установка состояния не изменяет переменную в текущем рендере, но запустит новый рендер.
- React выполнит обновления стейта после обработчиков событий который успешно выполнились. Это называется группировка.
- Чтобы обновить любое состояние множество раз за одно событие, можно использовать функцию обновления
setNumber(n => n + 1)
.
Challenge 1 of 2: Исправление счетчика запросов
Вы работаете над интернет-магазином произведений искусства, в котором пользователю можно делать множество заказов предметов искусства за один раз. Каждый раз когда пользователь нажимает кнопку “Купить”, счетчик “Ожидание” должен увеличиться на единицу. После 3 секунд, “Ожидание” счетчик должен уменьшаться, а счетчик “Выполнено” должен увеличиваться.
Однако, счетчик “Ожидание” не работает как задумано. Когда вы нажимаете “Купить”, значение уменьшается до -1
(чего вообще не должно быть). И если вы быстро кликните дважды, оба счетчика сработают непредсказуемо.
Почему это происходит? Давайте починим оба счетчика.
import { useState } from 'react'; export default function RequestTracker() { const [pending, setPending] = useState(0); const [completed, setCompleted] = useState(0); async function handleClick() { setPending(pending + 1); await delay(3000); setPending(pending - 1); setCompleted(completed + 1); } return ( <> <h3> Ожидание: {pending} </h3> <h3> Выполнено: {completed} </h3> <button onClick={handleClick}> Купить </button> </> ); } function delay(ms) { return new Promise(resolve => { setTimeout(resolve, ms); }); }