Состояние как снимок

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

You will learn

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

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

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

Рассмотрим пример кода ниже. Когда вы нажимаете на кнопку “Отправить”, setIsSent(true) сообщает React о необходимости повторного рендера UI:

import { useState } from 'react';

export default function Form() {
  const [isSent, setIsSent] = useState(false);
  const [message, setMessage] = useState('Привет!');
  if (isSent) {
    return <h1>Ваше сообщение уже в пути!</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">Отправить</button>
    </form>
  );
}

function sendMessage(message) {
  // ...
}

Итак, что же происходит, когда вы нажимаете на кнопку:

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

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

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

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

Если сравнивать с фотографией или кадром из фильма, снимок UI имеет интерактивность. Снимок включает в себя обработчики событий, которые определяют, что будет происходить в ответ на пользовательские действия. React обновляет интерфейс в соответствии со снимком и подключает обработчики событий. Например, нажатие на кнопку вызовет обработчик клика из вашего JSX.

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

  1. React вызывает ваш компонент как функцию.
  2. Функция возвращает новый снимок JSX.
  3. Далее React обновляет экран в соответствии с данными, которые были возвращены со снимком JSX.
  1. React вызывает функцию
  2. Создаёт снимок JSX
  3. Обновляет DOM-дерево

Illustrated by Rachel Lee Nabors

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

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

Illustrated by 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, вы просто трижды установите number значение 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>

Нажатие на кнопку установит number значение 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);

А что, если вы установите таймер для вашего предупреждения alert, дождётесь первого вызова setNumber(number + 5) и повторного рендера? Каким будет значение: 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 создавал снимок UI при вызове компонента.

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

  1. Вы нажимаете на кнопку “Отправить”, отправляя “Привет” для Элис.
  2. До того, как истечёт пятисекундный таймер, вы успеете поменять значение поля “Кому” на “Бобу”.

Какой результат вы ожидаете увидеть в alert? Будет отображено “Вы сказали Привет Элис” или же “Вы сказали Привет Бобу”? Попробуйте предположить, опираясь на знания, которые вы получили ранее, после запустите пример и посмотрите результат:

import { useState } from 'react';

export default function Form() {
  const [to, setTo] = useState('Элис');
  const [message, setMessage] = useState('Привет');

  function handleSubmit(e) {
    e.preventDefault();
    setTimeout(() => {
      alert(`Вы сказали ${message} ${to}`);
    }, 5000);
  }

  return (
    <form onSubmit={handleSubmit}>
      <label>
        Кому:{' '}
        <select
          value={to}
          onChange={e => setTo(e.target.value)}>
          <option value="Элис">Элис</option>
          <option value="Бобу">Бобу</option>
        </select>
      </label>
      <textarea
        placeholder="Message"
        value={message}
        onChange={e => setMessage(e.target.value)}
      />
      <button type="submit">Отправить</button>
    </form>
  );
}

React сохраняет значения состояния “фиксированными” в обработчиках событий одного рендера. Тем самым, вам не нужно беспокоиться о том, изменилось ли состояние во время выполнения кода.

Что, если вы хотите прочитать последнее состояние перед повторным рендером? Для этого вам необходимо использовать функцию обновления состояния, которая описана в следующей теме!

Recap

  • Обновление состояния всегда вызывает новый рендер.
  • React хранит в памяти состояние вне компонента, как будто состояние всегда находится на полке.
  • Когда вы вызываете useState, React отдаёт вам снимок состояния для текущего рендера.
  • Переменные и обработчики событий не “выживают” при повторном рендере. Каждый рендер имеет свои обработчики и переменные.
  • Каждый рендер (и функции внутри него) всегда будет “видеть” снимок состояния, который React дал этому рендеру.
  • Вы можете мысленно заменить состояние в обработчиках событий, подобно тому, как вы представляете себе отрендеренный JSX.
  • Обработчики событий, созданные в прошлом, имеют значения состояния из рендера, в котором они были созданы.

Challenge 1 of 1:
Реализовать светофор

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

import { useState } from 'react';

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

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

  return (
    <>
      <button onClick={handleClick}>
        Изменить на {walk ? 'Стоп' : 'Можно идти'}
      </button>
      <h1 style={{
        color: walk ? 'darkgreen' : 'darkred'
      }}>
        {walk ? 'Можно идти' : 'Стоп'}
      </h1>
    </>
  );
}

Вам необходимо добавить alert в обработчик кликов. Когда загорается зелёный свет и появляется текст “Можно идти”, должно быть оповещение “Дальше Стоп”. Когда свет красный и текст “Стоп”, должно быть оповещение “Дальше Можно идти”.

Имеет ли значение, помещаете ли вы alert до или после вызова setWalk?