Spec-Zone.ru › React 18

Очередь обновлений состояния

Установка переменной состояния поставит в очередь следующее отрисовку. Но иногда вам может потребоваться выполнить несколько операций со значением перед постановкой в очередь следующей отрисовки. Для этого нужно понять, как 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>
    </>
  )
}

Однако, как вы, возможно, помните из предыдущего раздела, значения состояния каждого рендера фиксируются, поэтому значение number внутри обработчика события первого рендера всегда 0, независимо от того, сколько раз вы вызываете setNumber(1):

setNumber(0 + 1);
setNumber(0 + 1);
setNumber(0 + 1);

Но здесь есть еще один фактор. React ожидает, пока все код в обработчиках событий выполнится, прежде чем обработать ваши обновления состояния. Именно поэтому перерисовка происходит только после всех этих setNumber() вызовов.

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

An elegant cursor at a restaurant places and order multiple times with React, playing the part of the waiter. After she calls setState() multiple times, the waiter writes down the last one she requested as her final order.

Иллюстрация Рэйчел Ли Наборс

Это позволяет вам обновлять несколько переменных состояния — даже из нескольких компонентов — без запуска слишком многих перерисовок. Но это также означает, что пользовательский интерфейс не будет обновлен до после выполнения вашего обработчика событий и любого кода в нем. Это поведение, также известное как группировка, делает ваш 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 называется функцией обновления. Когда вы передаете её установщику состояния:

  1. React помещает эту функцию в очередь для обработки после выполнения всего остального кода в обработчике событий.
  2. Во время следующей перерисовки React проходит по очереди и предоставляет вам окончательное обновленное состояние.
setNumber(n => n + 1);
setNumber(n => n + 1);
setNumber(n => n + 1);

Вот как React работает с этими строками кода при выполнении обработчика событий:

  1. setNumber(n => n + 1): n => n + 1 — это функция. React добавляет её в очередь.
  2. setNumber(n => n + 1): n => n + 1 — это функция. React добавляет её в очередь.
  3. 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:

  1. setNumber(number + 5): number равно 0, поэтому setNumber(0 + 5). React добавляет «заменить на 5» в свою очередь.
  2. 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 работает с этими строками кода во время выполнения этого обработчика событий:

  1. setNumber(number + 5): number равно 0, поэтому setNumber(0 + 5). React добавляет «заменить на 5» в свою очередь.
  2. setNumber(n => n + 1): n => n + 1 — это функция обновления. React добавляет эту функцию в свою очередь.
  3. 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) функцию обновления.

Попробуйте решить некоторые задачи

Задача 1 из 2:
Исправить счетчик запросов

Вы работаете над приложением для арт-рынка, которое позволяет пользователю отправлять сразу несколько заказов на предмет искусства. Каждый раз, когда пользователь нажимает кнопку «Купить», счетчик «Ожидание» должен увеличиваться на единицу. Через три секунды счетчик «Ожидание» должен уменьшиться, а счетчик «Выполнено» — увеличиться.

Однако счетчик «Ожидание» не ведет себя должным образом. При нажатии «Купить» он уменьшается до -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: {pending}
      </h3>
      <h3>
        Completed: {completed}
      </h3>
      <button onClick={handleClick}>
        Buy     
      </button>
    </>
  );
}

function delay(ms) {
  return new Promise(resolve => {
    setTimeout(resolve, ms);
  });
}

© 2013–present Facebook Inc.
Licensed under the Creative Commons Attribution 4.0 International Public License.
https://react.dev/learn/queueing-a-series-of-state-updates

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API