Что изменится в реальном DOM при клике в React

Дан блок кода:

import React, { useState } from 'react';

function SimpleButton(props) {
  const [txt, setTxt] = useState(props.text || 'a');
  const changeText = () => {
    setTxt(txt + txt);
  };

  return (
    <button onClick={changeText}> 
      {txt}
    </button>
  );
}

Что произойдёт в реальном DOM при клике на кнопку в данном React-компоненте?

Компонент будет вызываться так:

<SimpleButton />

Что произойдёт при клике пошагово

Исходный компонент:

import React, { useState } from 'react';

function SimpleButton(props) {
  const [txt, setTxt] = useState(props.text || 'a');
  const changeText = () => {
    setTxt(txt + txt);
  };

  return (
    <button onClick={changeText}>
      {txt}
    </button>
  );
}

Компонент вызывается как <SimpleButton />, значит props.text отсутствует и начальное состояние будет 'a'.

  1. Первый рендер создаёт DOM-узел кнопки и текстовый узел со значением a внутри неё.
  2. Клик вызывает обработчик changeText, который вызывает setTxt(txt + txt) и тем самым ставит обновление state в очередь.
  3. Обновление state автоматически триггерит повторный рендер компонента (React снова вызывает функцию компонента, чтобы понять, что должно быть на экране).
  4. На повторном рендере результат становится эквивалентен <button>aa</button>, потому что txt будет 'aa' на следующем рендере.
  5. В commit-фазе React применит минимальные изменения к реальному DOM: DOM-узел button останется тем же, а текстовый узел внутри изменится с a на aa.

Схема “триггер → рендер → commit”:

Клик
↓
setTxt(...) ставит обновление state в очередь
↓
(Trigger) React решает, что нужен ререндер
↓
(Render) React снова вызывает SimpleButton()
↓
(Diff) Находит отличие: изменился только {txt}
↓
(Commit) Меняет текстовый узел в DOM (a → aa)

Ключевой факт: “рендер” в React — это вызов функций компонентов и вычисление результата, а непосредственные изменения DOM происходят на стадии commit.

Почему кнопка не пересоздаётся

При обновлении state React сначала строит новое описание UI и сравнивает его с предыдущим, чтобы понять, что изменилось.
Если тип элемента и его положение в дереве совпадают (в обоих рендерах возвращается button на том же месте), React сохраняет существующий DOM-узел и обновляет только то, что отличается.
В данном компоненте отличается только значение {txt}, поэтому достаточно обновить текст, а не удалять и создавать button заново.

Пример “до/после” на уровне результата рендера:

// До клика:
<button>a</button>

// После клика:
<button>aa</button>
Пересоздание DOM-узла возможно в других ситуациях, например если меняется тип элемента (условный рендер возвращает то button, то div) — тогда React будет вынужден заменить узел целиком.

Что с onClick и почему “атрибут” не меняется

В JSX запись onClick={changeText} выглядит как “атрибут”, но React использует собственную систему событий и делегирование, чтобы эффективно обрабатывать события.
В реальном DOM это обычно означает, что на каждом button не появляется/не обновляется “HTML-атрибут onclick”, а обработка кликов идёт через общий слушатель на корневом контейнере и внутреннее сопоставление “где кликнули” → “какой обработчик вызвать”.
Поэтому при клике в рассматриваемом примере нет необходимости менять DOM-атрибут onClick; достаточно обновить state и текст.

В React 17+ обработчики событий привязаны к корневому контейнеру приложения (а не глобально к document), что является частью реализации делегирования событий.

Дополнительный важный нюанс про state в обработчике:

  • Вызов setTxt(txt + txt) использует значение txt, которое было актуально на момент выполнения обработчика, а само новое значение будет доступно со следующего рендера.
  • Если в одном и том же событии планируется несколько обновлений, то корректнее вычислять новое состояние через функцию-обновитель.

Вариант с updater-функцией (стабильнее при нескольких обновлениях и батчинге):

const changeText = () => {
  setTxt(prev => prev + prev);
};

React также может группировать несколько обновлений состояния и обновляет экран после завершения обработчиков событий, чтобы не делать лишние промежуточные перерисовки.

Итого: при клике setTxt запускает повторный рендер, и в реальном DOM сохраняется тот же узел button, но обновляется его текст (например, a → aa); обработчик onClick как HTML-атрибут обычно не переустанавливается, потому что React использует делегирование событий и синтетическую систему событий.