Перейти к содержимому
Тема 6 из 13
Теория

Как React обновляет страницу

Почему это важно

В прошлых темах мы писали setCount(count + 1), и «React перерисовывал компонент». Пора разобраться, что именно при этом происходит. Без этого React остаётся магией, а большинство ошибок новичков — «почему экран не обновился», «почему значение старое», «почему поле не очистилось» — непонятны. После этой темы ты сможешь предсказать, что сделает React, ещё до запуска кода.

Три шага: вызвать, сравнить, применить

Любое обновление экрана в React проходит три шага:

  1. Запуск (trigger). Что-то просит обновления: первая отрисовка root.render(...) или вызов функции set… из useState.
  2. Отрисовка (render). React вызывает функции-компоненты. Каждая возвращает JSX — описание того, что должно быть на экране. Сама страница на этом шаге ещё не меняется.
  3. Применение (commit). React сравнивает новое описание с предыдущим и меняет в настоящем DOM только то, что отличается. После этого запускаются эффекты (useEffect).

«Отрисовка» в React — это не рисование пикселей, а просто вызов твоей функции.

Что возвращает JSX

JSX — это не HTML. Сборщик превращает каждый тег в вызов React.createElement, а тот возвращает обычный объект:

const element = <h1 className="title">Привет</h1>;
// после сборки:
const element = React.createElement("h1", { className: "title" }, "Привет");
// это объект примерно такого вида:
// { type: "h1", props: { className: "title", children: "Привет" } }

Дерево таких объектов называют виртуальным DOM. Создавать объекты очень дёшево, а вот менять настоящую страницу дорого: браузеру приходится пересчитывать расположение и перерисовывать пиксели. Поэтому React каждый раз строит новое лёгкое дерево целиком, а в настоящий DOM вносит минимальные правки.

Сравнение деревьев

Сравнение старого и нового дерева называется reconciliation (согласование). Правила простые:

  • элемент того же типа на том же месте — React оставляет DOM-узел и меняет только изменившиеся атрибуты и текст;
  • другой тип (<p> стал <div>, <Chat> стал <Profile>) — старый узел со всем, что внутри, удаляется, создаётся новый;
  • в списках элементы сопоставляются по key, а не по порядку.
function Counter() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <h1>Счётчик</h1>
      <p>Нажато: {count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
}

После клика React вызывает Counter снова, получает новое дерево и видит: div, h1 и button не изменились, а у p поменялся текст. В DOM меняется один текстовый узел. Если в поле ввода на странице был курсор, он не пропадёт: поле никто не пересоздавал.

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

Компонент вызывается заново, когда:

  1. изменилось его состояние (вызвали set… с новым значением);
  2. перерисовался его родитель — тогда перерисовываются все дети, даже если их props не менялись;
  3. изменился контекст, который он читает (тема про useContext).

Убедиться можно, добавив console.log прямо в тело компонента:

function Child({ label }) {
  console.log("render Child", label);
  return <p>{label}</p>;
}

function Parent() {
  const [n, setN] = useState(0);
  console.log("render Parent", n);
  return (
    <div>
      <button onClick={() => setN(n + 1)}>{n}</button>
      <Child label="я не меняюсь" />
    </div>
  );
}

Каждый клик печатает и render Parent, и render Child. Это нормально и почти всегда быстро: отрисовка — лишь вызов функции, а DOM для Child не меняется. Как пропускать лишние отрисовки, когда это действительно нужно, — в теме про производительность.

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

Состояние — это снимок

Внутри одной отрисовки значение состояния не меняется. count — обычная константа той отрисовки, в которой создан обработчик:

function Counter() {
  const [count, setCount] = useState(0);

  function handleClick() {
    setCount(count + 1);
    console.log(count);       // всё ещё 0!
  }

  return <button onClick={handleClick}>{count}</button>;
}

setCount не меняет переменную count. Он говорит React: «в следующей отрисовке дай count значение 1». Новое значение появится, только когда React снова вызовет Counter.

Пачки обновлений

Если в одном обработчике вызвать несколько set…, React не перерисовывает компонент после каждого: он собирает их в пачку (batching) и делает одну отрисовку после обработчика.

function handleClick() {
  setCount(count + 1);   // count = 0 → «поставь 1»
  setCount(count + 1);   // count всё ещё 0 → «поставь 1»
  setCount(count + 1);   // «поставь 1»
}
// итог: 1, а не 3

Если новое значение зависит от предыдущего, передай функцию обновления. React поставит такие функции в очередь и вызовет их по порядку, каждой давая результат предыдущей:

function handleClick() {
  setCount((c) => c + 1);   // 0 → 1
  setCount((c) => c + 1);   // 1 → 2
  setCount((c) => c + 1);   // 2 → 3
}

Правило: если следующее значение считается из текущего — пиши set((старое) => новое).

Нельзя менять состояние напрямую

Как React узнаёт, что состояние изменилось? Сравнивает старое и новое значение через Object.is (почти то же, что ===). Для объектов и массивов это сравнение ссылок, а не содержимого:

const [items, setItems] = useState(["a", "b"]);

function addWrong() {
  items.push("c");      // изменили тот же массив
  setItems(items);      // та же ссылка → React: «ничего не изменилось» → отрисовки нет
}

function addRight() {
  setItems([...items, "c"]);   // новый массив → новая ссылка → отрисовка
}

Поэтому состояние в React считают неизменяемым: вместо изменения создают новую копию.

Действие Массив Объект
Добавить [...arr, x] { ...obj, key: value }
Удалить arr.filter((x) => x.id !== id) const { key, ...rest } = obj
Изменить arr.map((x) => (x.id === id ? { ...x, done: true } : x)) { ...obj, key: newValue }
Сортировать [...arr].sort(...) —

Методы push, splice, sort, reverse и присваивание obj.key = … меняют исходник — для состояния их не используют (или сначала делают копию).

Помни, что копия через ... — поверхностная: вложенные объекты нужно копировать тоже ({ ...user, address: { ...user.address, city: "Худжанд" } }).

Где живёт состояние: место в дереве

Состояние хранится не «внутри функции», а в React — привязанным к месту компонента в дереве. Пока на этом месте стоит компонент того же типа, состояние сохраняется, даже если props поменялись:

{selected === "ann" ? <Chat contact={ann} /> : <Chat contact={timur} />}

Для React здесь один и тот же <Chat> на одном месте — у него просто поменялись props. Черновик сообщения, набранный для Ани, останется и для Тимура. Иногда это ошибка.

Чтобы сказать «это другой экземпляр, начни с чистого листа», используют key — не только в списках:

<Chat key={contact.id} contact={contact} />

Новый key — React удаляет старый компонент вместе с состоянием и создаёт новый. Это обычный способ сбросить форму или виджет при смене выбранного элемента.

Обратное тоже верно: если компонент пропал из дерева (например, через {show && <Panel />}), его состояние уничтожается. Когда он появится снова, начнёт с начальных значений.

Строгий режим

В проектах приложение обычно оборачивают в <React.StrictMode>. В режиме разработки он нарочно вызывает компоненты дважды и монтирует эффекты дважды (запуск → очистка → запуск), чтобы быстрее найти нечистые компоненты и эффекты без очистки. Если видишь двойные console.log — это он. В готовой сборке двойных вызовов нет.

Итог

  • Обновление: запуск (set…) → отрисовка (вызов компонентов) → применение (минимальные правки DOM) → эффекты.
  • JSX — это объекты; React сравнивает новое дерево со старым и меняет только разницу.
  • Компонент перерисовывается при изменении своего состояния, при отрисовке родителя и при изменении контекста. Компонент — чистая функция.
  • Состояние — снимок отрисовки; обновления собираются в пачку. Из старого значения — set((old) => ...).
  • Состояние не меняют напрямую: новые массивы и объекты через ..., map, filter.
  • Состояние привязано к месту в дереве; key создаёт новый экземпляр и сбрасывает состояние.

Плюс три

Лёгкая+10 XP

Компонент TripleCounter показывает число в <span> и кнопку +3. По задумке кнопка прибавляет 3, но в коде ошибка: после клика число увеличивается только на 1.

Исправь TripleCounter, оставив три вызова setCount (так проще увидеть, как React обрабатывает обновления).

0 [+3]  → клик →  3  → клик →  6

Подключи коннектор, чтобы решать задачи

Код задач выполняется на твоём компьютере — так быстрее и надёжнее, чем в браузере. Для этого нужна бесплатная программа-коннектор CodeWithMe. Подключить её нужно один раз.

Ищем коннектор на этом компьютере…

  1. 1
    Скачай коннектор для своей системы (Windows, macOS или Linux).Скачать коннекторНа странице загрузки выбери файл со своей системой в названии, например windows-amd64.
  2. 2Запусти его и выбери папку, где будут лежать твои решения. Python и Node.js он поставит туда сам, когда понадобится.
  3. 3
    Введи ниже код из 6 цифр — он написан в окне коннектора («Код для сайта CodeWithMe»). Если браузер спросит про доступ к локальной сети — разреши.

Коннектор открытый (исходный код на GitHub), работает только на твоём компьютере и выполняет код лишь после ввода кода сопряжения.