Spec-Zone.ru › React 18

Состояние как моментальный снимок

Переменные состояния могут выглядеть как обычные переменные JavaScript, которые можно читать и изменять. Однако состояние ведет себя скорее как моментальный снимок. Установка его не изменяет уже имеющуюся переменную состояния, а вместо этого вызывает повторную отрисовку.

Вы узнаете

  • Как установка состояния вызывает повторные отрисовки
  • Когда и как обновляется состояние
  • Почему состояние не обновляется сразу после установки
  • Как обработчики событий получают «моментальный снимок» состояния

Установка состояния вызывает повторные отрисовки

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

В этом примере, когда вы нажимаете «отправить», 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) {
  // ...
}

Вот что происходит при нажатии на кнопку:

  1. Обработчик события onSubmit выполняется.
  2. setIsSent(true) устанавливает isSent в true и добавляет в очередь новую отрисовку.
  3. React перерисовывает компонент в соответствии с новым значением isSent.

Давайте более подробно рассмотрим взаимосвязь между состоянием и отрисовкой.

Отрисовка создаёт моментальный снимок во времени

“Отрисовка” означает, что React вызывает ваш компонент, который является функцией. JSX, который вы возвращаете из этой функции, подобен моментальному снимку пользовательского интерфейса в определённый момент времени. Его свойства, обработчики событий и локальные переменные были все рассчитаны с использованием его состояния на момент отрисовки.

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

Когда React перерисовывает компонент:

  1. React снова вызывает вашу функцию.
  2. Ваша функция возвращает новый моментальный снимок JSX.
  3. Затем React обновляет экран в соответствии с возвращённым вами снимком.
  1. React выполняет функцию
  2. Вычисление моментального снимка
  3. Обновление дерева DOM

Иллюстрация предоставлена Rachel Lee Nabors

В качестве памяти компонента, состояние не похоже на обычную переменную, которая исчезает после возврата вашей функции. Состояние фактически «живёт» в самом React — как будто на полке! — вне вашей функции. Когда React вызывает ваш компонент, он предоставляет вам моментальный снимок состояния для этой конкретной отрисовки. Ваш компонент возвращает моментальный снимок пользовательского интерфейса с новым набором свойств и обработчиков событий в своём JSX, все вычисленные с использованием значений состояния из этой отрисовки!

  1. Вы сообщаете React об обновлении состояния
  2. React обновляет значение состояния
  3. React передаёт моментальный снимок значения состояния в компонент

Иллюстрация предоставлена Rachel Lee Nabors

Вот небольшой эксперимент, чтобы показать, как это работает. В этом примере вы можете ожидать, что нажатие кнопки «+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:

  1. setNumber(number + 1): number равно 0, поэтому setNumber(0 + 1).
    • React готовится изменить number на 1 при следующей отрисовке.
  2. setNumber(number + 1): number равно 0, поэтому setNumber(0 + 1).
    • React готовится изменить number на 1 при следующей отрисовке.
  3. 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>
    </>
  )
}

Если вы используете метод подстановки из предыдущего примера, вы можете предположить, что всплывающее окно отобразит «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>
    </>
  )
}

Удивлены? Если вы используете метод подстановки, вы можете увидеть «снимок» состояния, переданного всплывающему окну.

setNumber(0 + 5);
setTimeout(() => {
  alert(0);
}, 3000);

Состояние, хранящееся в React, может измениться к тому времени, когда запустится всплывающее окно, но оно было запланировано с использованием снимка состояния на момент взаимодействия пользователя!

Значение переменной состояния никогда не изменяется в рамках одной отрисовки, даже если код обработчика событий асинхронный. Внутри обработчика этой отрисовки onClick, значение number продолжает быть 0, даже после вызова setNumber(number + 5). Его значение «зафиксировано» при создании React «моментального снимка» пользовательского интерфейса путём вызова вашего компонента.

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

  1. Вы нажимаете кнопку «Отправить», отправляя «Привет» Алисе.
  2. Перед тем, как закончится пятисекундная задержка, вы меняете значение поля «Кому» на «Боб».

Что вы ожидаете увидеть в alert? Оно отобразит «Вы сказали Привет Алисе»? Или оно отобразит «Вы сказали Привет Бобу»? Сделайте предположение, исходя из того, что вы знаете, а затем попробуйте:

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.
  • Обработчики событий, созданные ранее, имеют значения состояния из отрисовки, в которой они были созданы.

Попробуйте выполнить некоторые задания

Задание 1 из 1:
Реализуйте светофор

Вот компонент светофора пешеходного перехода, который переключается при нажатии кнопки:

import { useState } from 'react';

export default function TrafficLight() {
  const [walk, setWalk] = useState(true);

  function handleClick() {
    setWalk(!walk);
  }

  return (
    <>
      <button onClick={handleClick}>
        Change to {walk ? 'Stop' : 'Walk'}
      </button>
      <h1 style={{
        color: walk ? 'darkgreen' : 'darkred'
      }}>
        {walk ? 'Walk' : 'Stop'}
      </h1>
    </>
  );
}

Добавьте alert к обработчику щелчка. Когда свет зелёный и говорит «Идите», нажатие кнопки должно сказать «Следующее - Стоп». Когда свет красный и говорит «Стоп», нажатие кнопки должно сказать «Следующее - Идите».

Влияет ли то, поставили ли вы alert перед или после вызова setWalk?

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

Spec-Zone.ru

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