Frontend-разработка React: компоненты, JSX, состояние, жизненный цикл, реконсиляция
0%

React: компоненты, JSX, состояние, жизненный цикл, реконсиляция

React: компоненты, JSX, состояние, жизненный цикл, реконсиляция

В предыдущей статье мы собрали проект: Vite, модули, code splitting. Теперь — что именно мы собираем. React занимает в вакансиях и репозиториях место, несоразмерное его размеру: это библиотека на ~45 КБ (gzip, вместе с react-dom), которая делает ровно одну вещь — держит DOM синхронным с данными. Всё остальное — роутинг, запросы, стили, формы — экосистема вокруг.

Проблема в том, что большинство разработчиков учит React как набор заклинаний: «в зависимости useEffect положи это», «оберни в useCallback», «поставь key». Заклинания ломаются на первой нестандартной ситуации. Эта статья про модель: что такое элемент, чем рендер отличается от отрисовки, как работает сопоставление деревьев и почему setState внутри обработчика не меняет переменную сразу. Разобравшись в модели, вы перестанете угадывать.

Язык мы здесь не обсуждаем — типы, дженерики и настройка tsconfig разобраны в треке TypeScript. Код ниже на TypeScript просто потому, что так пишут в проде.

Задача: императивный DOM не масштабируется

Напишем счётчик подписчиков вручную, без библиотек.

// Императивно: мы описываем ПОСЛЕДОВАТЕЛЬНОСТЬ ДЕЙСТВИЙ над DOM
let count = 0;
document.querySelector('#btn').addEventListener('click', () => {
  count += 1;
  label.textContent = `Подписчиков: ${count}`;  // не забыть обновить
  badge.hidden = count === 0;                    // и это тоже
  button.disabled = count >= 100;                // и это
  document.title = `${count} подписчиков`;       // а вот это забыли
});

Каждое новое состояние требует новой ветки кода: «если стало ноль — спрятать бейдж, но не забыть про заголовок». Число переходов между состояниями растёт квадратично от числа состояний, и баги живут именно в непокрытых переходах. Классический симптом — «после третьего клика по фильтру список показывает старые данные».

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

function Subscribers() {
  const [count, setCount] = useState(0);
  // Одна функция описывает ВСЕ возможные состояния разом
  return (
    <>
      <p>Подписчиков: {count}</p>
      {count > 0 && <span className="badge">новое</span>}
      <button onClick={() => setCount(count + 1)} disabled={count >= 100}>
        Подписаться
      </button>
    </>
  );
}

Формально: UI = f(state). Это та же идея, что и в декларативном CSS или в SQL — вы говорите «что», а не «как». Цена известна: между вашим кодом и DOM появляется прослойка, которая должна вычислить разницу. Дальше — как именно она это делает.

JSX: не HTML, не шаблон, а вызовы функций

<div className="row">{name}</div> — это не строка и не HTML. Это синтаксический сахар, который транспилятор (Babel, SWC, esbuild в Vite) превращает в вызов функции. С React 17 действует «новый JSX-трансформ»: импорт React больше не нужен, вызывается jsx из react/jsx-runtime.

// Исходник
const el = <Card title="Отчёт" onClose={close}><p>Тело</p></Card>;

// Во что превращает SWC (упрощённо)
import { jsx as _jsx } from "react/jsx-runtime";
const el = _jsx(Card, {
  title: "Отчёт",
  onClose: close,
  children: _jsx("p", { children: "Тело" })
});

// А _jsx возвращает обычный неизменяемый объект — «элемент»
// { $$typeof: Symbol(react.element), type: Card, key: null, props: {...} }

Из этого следуют все правила JSX, которые обычно учат наизусть:

  • Один корень. Функция возвращает одно значение. Нужно несколько соседей — <>...</> (Fragment), он не создаёт узел DOM.
  • className, htmlFor, tabIndex. Ключи объекта — свойства DOM-узла, а class и for — зарезервированные слова JS.
  • Заглавная буква имеет значение. <div> компилируется в jsx("div", ...) — строка, значит хост-элемент платформы. <Card> компилируется в jsx(Card, ...) — ссылка на функцию, значит компонент. Написали <card> — React честно попробует создать неизвестный HTML-тег.
  • Фигурные скобки — это выражение, а не оператор. {if (x) ...} невозможно; работают тернарник, &&, вызов функции.
  • Элемент — описание, а не то, что на экране. Создать его дёшево: это литерал объекта. Он не «монтируется», пока React не решит его отрендерить.

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

Отсюда же классическая ловушка условного рендера: {items.length && <List/>} при пустом массиве выведет на экран текст «0». React не рисует false, null, undefined и true, но рисует 0 и пустую строку — это валидные текстовые узлы, а && возвращает левый операнд, если он ложный. Правильно — {items.length > 0 && <List/>}.

Компонент: функция из props в элементы

Компонент — функция, принимающая объект props и возвращающая элементы. Всё. Классы существуют, но новый код на них не пишут: с 2019 года весь дизайн API строится вокруг функций и хуков.

type BadgeProps = {
  children: React.ReactNode;
  tone?: 'neutral' | 'success' | 'danger';
};

// Компонент — обычная функция. Props только читаются, никогда не мутируются.
export function Badge({ children, tone = 'neutral' }: BadgeProps) {
  return <span className={`badge badge--${tone}`}>{children}</span>;
}

Данные текут вниз, события — вверх. Родитель передаёт значения через props, ребёнок сообщает о намерении через колбэк. Ребёнок не может изменить props — это гарантия, на которой держится вся предсказуемость: увидев значение props, вы знаете, что оно пришло ровно из одного места.

Композиция вместо конфигурации

Первая архитектурная развилка, на которой ошибаются: наращивать props или вкладывать элементы.

// ПЛОХО: props растут бесконечно, компонент знает про все случаи
<Dialog title="Удалить" icon="trash" showFooter footerAlign="right"
        primaryText="Удалить" onPrimary={del} secondaryText="Отмена" />

// ХОРОШО: слоты. Dialog отвечает за поведение, содержимое даёт вызывающий
<Dialog onClose={close}>
  <Dialog.Title>Удалить проект?</Dialog.Title>
  <Dialog.Body>Действие необратимо.</Dialog.Body>
  <Dialog.Footer>
    <Button onClick={close}>Отмена</Button>
    <Button tone="danger" onClick={del}>Удалить</Button>
  </Dialog.Footer>
</Dialog>

children — не магия, а обычный prop типа ReactNode, и слотов может быть несколько: <Layout sidebar={<Nav />}>...</Layout> — это просто ещё один prop, в который передали элемент. Побочный эффект композиции — производительность: элемент, переданный через children, создан в родителе и при ре-рендере Layout не пересоздаётся, поэтому React пропустит его поддерево. Об этом ещё вспомним в разделе про тормоза.

Правила React: рендер должен быть чистым

Функция компонента вызывается React в момент, который вы не контролируете, и может быть вызвана несколько раз подряд или брошена на полпути. Отсюда жёсткие правила (react.dev/reference/rules):

  1. Одинаковые props и состояние → одинаковый результат. Никаких Math.random() и new Date() в теле рендера, если результат влияет на разметку.
  2. Никаких мутаций: ни props, ни состояния, ни переменных модуля, ни DOM напрямую.
  3. Никаких запросов, таймеров и подписок в теле функции — только в обработчиках событий и эффектах.
function Report({ rows }: { rows: Row[] }) {
  // rows.sort(...) — ПЛОХО: мутирует чужой массив прямо в рендере
  return <Table rows={rows.toSorted((a, b) => a.date - b.date)} />;  // ХОРОШО: копия
}

<StrictMode> в разработке специально вызывает компоненты и эффекты дважды, чтобы нечистота проявилась сразу, а не в проде под конкурентным рендером. Если приложение ломается в StrictMode — оно сломано, просто вы этого ещё не видели. Отключать StrictMode ради «двойных запросов» — лечить симптом.

Рендер — это не отрисовка

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

Цикл состоит из трёх шагов.

  1. Триггер — первый рендер (createRoot().render()) или обновление состояния.
  2. Фаза render — React вызывает компоненты, строит новое дерево, сравнивает со старым. Чистая, без побочных эффектов, прерываемая.
  3. Фаза commit — React применяет накопленные изменения к DOM. Синхронная, атомарная, непрерываемая.

Ключевое следствие: рендер не обязательно означает работу с DOM. Компонент может выполниться сто раз, и если результат совпал со старым — React не тронет ни один узел. Обратное тоже верно: «лишний рендер» — это лишние вызовы функций и сравнение объектов, обычно микросекунды. Оптимизировать нужно не рендеры вообще, а конкретный медленный рендер, найденный профайлером.

Состояние: снимок, а не переменная

Почему нельзя обычную переменную? Потому что при следующем вызове функции она создастся заново, а React не узнает, что нужно перерендерить. const [value, setValue] = useState(initial) решает обе задачи: хранит значение вне вызова функции (в узле Fiber) и уведомляет React об изменении.

Правило первое: состояние в рендере — снимок

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

  function handleTripleClick() {
    setCount(count + 1);   // count === 0 → планируем 1
    setCount(count + 1);   // count всё ещё 0 → планируем 1
    setCount(count + 1);   // count всё ещё 0 → планируем 1
    console.log(count);    // 0 — переменная не изменилась и не изменится
  }
  // после обработчика: count === 1, а не 3
}

count — обычная константа, захваченная замыканием текущего рендера. setCount её не меняет: он ставит в очередь обновление и просит React отрендерить компонент заново. При следующем вызове функции будет создана новая константа count с новым значением. Это не баг и не асинхронность в привычном смысле — это гарантия консистентности: внутри одного рендера все значения соответствуют одному моменту времени.

Правило второе: функциональный апдейтер читает актуальное значение

setCount(c => c + 1);   // c — значение из очереди, а не из замыкания
setCount(c => c + 1);
setCount(c => c + 1);   // итого +3

Апдейтеры складываются в очередь и применяются по порядку в момент рендера. Используйте эту форму всегда, когда новое значение зависит от старого — особенно внутри setInterval, setTimeout, промисов и подписок, где замыкание давно устарело.

Правило третье: батчинг

React объединяет все обновления, случившиеся в одном тике, в один рендер. С React 18 это работает везде: в обработчиках, в setTimeout, в .then(), в нативных событиях. Нужно выйти из батча (редко, обычно ради измерения DOM) — flushSync из react-dom.

Правило четвёртое: неизменяемость

React сравнивает состояние через Object.is. Мутировали объект — ссылка та же, обновления не будет.

// ПЛОХО: ссылка не изменилась, ре-рендера не будет
user.name = 'Аня'; setUser(user);
todos.push(newTodo); setTodos(todos);

// ХОРОШО: новая ссылка
setUser(u => ({ ...u, name: 'Аня' }));
setTodos(ts => [...ts, newTodo]);
setTodos(ts => ts.map(t => t.id === id ? { ...t, done: !t.done } : t));
setTodos(ts => ts.filter(t => t.id !== id));

// Глубокая вложенность — признак плохой формы состояния.
// Либо нормализуйте по id, либо возьмите Immer (useImmer).

Правило пятое: ленивая инициализация

// ПЛОХО: parse выполняется на КАЖДОМ рендере, результат выбрасывается
const [draft, setDraft] = useState(JSON.parse(localStorage.getItem('draft') ?? '{}'));

// ХОРОШО: функция вызывается только при монтировании
const [draft, setDraft] = useState(() => JSON.parse(localStorage.getItem('draft') ?? '{}'));

Как выбирать форму состояния

  • Не дублируйте то, что выводится. fullName из first + last — не состояние, а выражение в теле рендера. Дублирование = рассинхронизация.
  • Группируйте то, что меняется вместе. Координаты {x, y} — один объект, а не два useState.
  • Избегайте противоречивых комбинаций. Вместо isLoading + isError + data — один дискриминированный союз: { status: 'idle' | 'loading' | 'success' | 'error' }. Невалидное состояние должно быть невыразимым.
  • Держите состояние как можно ниже. Локальное состояние формы не должно жить в глобальном сторе. Подробнее — Управление состоянием.
  • Поднимайте вверх ровно до общего предка двух компонентов, которым оно нужно. Аккордеон, где открыта одна панель, хранит openIndex в родителе, а Panel получает isOpen и onOpen — сама она ничего не помнит.

Реконсиляция: как React сравнивает деревья

После рендера у React два дерева: то, что на экране, и то, что должно быть. Задача — найти минимальный набор операций. Классический алгоритм сравнения деревьев работает за O(n³) — для тысячи узлов это миллиард операций, неприемлемо. React использует эвристический алгоритм за O(n), опирающийся на два допущения, верных для реальных интерфейсов:

  1. Элементы разных типов дают разные деревья. Если на позиции был <div>, а стал <span> (или был <Chart>, а стал <Table>) — React не пытается их сопоставить: старое поддерево размонтируется целиком со всем состоянием и эффектами, новое монтируется с нуля.
  2. Разработчик подсказывает стабильность через key. Внутри списка соседей React сопоставляет детей по ключу, а не по позиции.

Если тип совпал — React оставляет узел DOM на месте, обновляет только изменившиеся атрибуты и рекурсивно идёт к детям.

Позиция в дереве — это идентичность

Состояние привязано не к компоненту, а к месту в дереве UI. Отсюда два симметричных сюрприза.

// СЮРПРИЗ 1: состояние сохраняется, хотя «компонент другой».
// Обе ветки дают Counter на первой позиции внутри div — это один и тот же fiber.
<div>{isFancy ? <Counter fancy /> : <Counter />}</div>

// СЮРПРИЗ 2: состояние сбрасывается при «косметическом» изменении разметки.
// section и div — разные типы, поддерево пересоздаётся.
{isFancy ? <section><Counter /></section> : <div><Counter /></div>}

Отсюда рабочий приём: key как способ сбросить состояние.

// Форма редактирования должна полностью обнулиться при смене пользователя
<UserForm key={userId} user={user} />

// Без key: React считает это тем же компонентом, поля сохранят чужие данные

Это официально рекомендованная замена устаревшему getDerivedStateFromProps и хаку с useEffect, сбрасывающим состояние.

Списки и key

Сопоставление списка по ключу и по индексу

{users.map((u, i) => <UserRow key={i} user={u} />)}             // ПЛОХО при вставках и сортировке
{users.map(u => <UserRow key={Math.random()} user={u} />)}      // ПЛОХО: список пересоздаётся весь
{users.map(u => <UserRow key={u.id} user={u} />)}               // ХОРОШО: стабильный id из данных

Требования к ключу: стабильный между рендерами, уникальный среди соседей (не глобально), выводимый из данных. Нет id с сервера — сгенерируйте его при создании элемента (crypto.randomUUID()) и храните в данных, а не вычисляйте в рендере. Индекс допустим только для списка, который никогда не меняет порядок и длину.

Симптомы неправильного ключа узнаваемы: отмеченный чекбокс «переезжает» на соседнюю строку, введённый текст остаётся в поле после удаления записи, CSS-анимация проигрывается не на том элементе, поле теряет фокус при каждом нажатии клавиши.

Fiber: как это устроено внутри

До 2017 года React обходил дерево рекурсивно, и обход нельзя было остановить: длинный рендер блокировал главный поток и ронял отзывчивость. Fiber — переписанное ядро, где узел дерева стал объектом с явными указателями, а рекурсия — циклом.

Дерево Fiber: указатели child, sibling, return и двойная буферизация

Поле Что хранит Зачем знать
tag вид узла: функция, хост-элемент, фрагмент, Suspense по нему React решает, как обрабатывать узел
type функция компонента или строка тега смена type = размонтирование поддерева
stateNode реальный узел DOM или корень связь виртуального дерева с платформой
child / sibling / return указатели обхода позволяют идти циклом, а не рекурсией
alternate тот же узел в другом дереве двойная буферизация без аллокаций
memoizedState связный список хуков вот почему хуки нельзя вызывать в условии
flags что сделать в коммите Placement, Update, Deletion
lanes битовая маска приоритетов ввод текста важнее фоновой загрузки

Из memoizedState как связного списка следует главное правило хуков: React не знает их имён, он идентифицирует хук порядком вызова. Поставили useState внутрь if — при следующем рендере третий хук получит состояние второго. Именно это ловит правило react-hooks/rules-of-hooks в ESLint.

Работа в фазе render — цикл while (workInProgress !== null), который на каждом шаге может проверить shouldYield() и вернуть управление браузеру. Отсюда конкурентные возможности: useTransition помечает обновление как несрочное (набор текста в поиске не блокируется перестройкой тяжёлого списка), Suspense даёт декларативную загрузку, useDeferredValue показывает устаревшее значение, пока считается новое. Подробности — в статьях о данных и рендеринге.

Отдельно стоит развеять миф: виртуальный DOM не быстрее прямых манипуляций. Прямой вызов textContent всегда дешевле, чем построить дерево объектов, сравнить его и потом вызвать тот же textContent. React выигрывает не в скорости, а в том, что даёт «достаточно быстро» без ручной оптимизации и без класса ошибок рассинхронизации. Это инженерный размен, а не превосходство.

Жизненный цикл функционального компонента

Классических componentDidMount и componentDidUpdate в функциях нет — и это не упрощение, а другая модель. Вместо «что сделать в такой-то момент» вы описываете синхронизацию с внешней системой: «пока компонент на экране с такими параметрами, должна существовать вот эта подписка».

function ChatRoom({ roomId }: { roomId: string }) {
  const [messages, setMessages] = useState<Message[]>([]);

  useEffect(() => {
    const socket = connect(roomId);              // «включить» синхронизацию
    socket.on('message', m => setMessages(p => [...p, m]));
    return () => socket.disconnect();            // «выключить» — обязательно
  }, [roomId]);                                  // сменился roomId → выключить старое, включить новое

  return <MessageList items={messages} />;
}

Функция очистки вызывается не только при размонтировании, но и перед каждым повторным запуском эффекта. Мысленная модель «монтирование/размонтирование» здесь вредна; правильная — «эффект должен уметь запускаться и останавливаться сколько угодно раз». Именно это проверяет двойной вызов в StrictMode.

Порядок в одном коммите: мутации DOM → useLayoutEffect (синхронно, до отрисовки; здесь измеряют геометрию и правят позицию тултипа, чтобы не мигало) → браузер красит кадр → useEffect (асинхронно, после отрисовки; всё остальное). Внутри дерева эффекты выполняются снизу вверх: дети раньше родителей.

Глубокий разбор useEffect, зависимостей, useRef, мемоизации и собственных хуков — в следующей статье.

Почему тормозит и как это измерять

Сначала диагноз, потом лечение. В React-приложении есть ровно четыре источника медленности, и лечатся они по-разному.

  1. Огромный бандл. Приложение не тормозит — оно ещё не приехало. Смотрите LCP и размер JS; лечится code splitting и React.lazy (сборка, производительность).
  2. Каскад рендеров. Состояние лежит слишком высоко: setState в корне перерисовывает всё дерево. Лечится опусканием состояния вниз, композицией через children и memo на границах.
  3. Дорогой отдельный рендер. Один компонент сортирует 50 000 строк прямо в теле функции. Лечится вынесением вычисления, виртуализацией списков (@tanstack/virtual), пагинацией.
  4. Дорогой коммит. Тысячи узлов DOM создаются разом; тормозит уже браузер — раскладка и отрисовка. Лечится сокращением числа узлов, content-visibility, виртуализацией.

Единственный способ отличить второе от третьего — профайлер. React DevTools Profiler показывает флейм-график коммитов: ширина полосы — время рендера компонента, а «Why did this render?» (включается в настройках) называет причину: props, hook, parent.

// Программное измерение — работает и в проде, если собрать с profiling-сборкой
import { Profiler } from 'react';

<Profiler
  id="ProductList"
  // actualDuration — с учётом мемоизации; baseDuration — сколько было бы без неё
  onRender={(id, phase, actualDuration) => {
    if (actualDuration > 16) analytics.track('slow_render', { id, phase, actualDuration });
  }}
>
  <ProductList items={items} />
</Profiler>

Пользователь же чувствует не «рендеры», а INP (Interaction to Next Paint) — время от нажатия до отрисовки следующего кадра. Бюджет: 200 мс на 75-м перцентиле. Замеряйте полем, а не в лаборатории:

// npm i web-vitals — отправляем реальные метрики пользователей
import { onINP, onLCP, onCLS } from 'web-vitals';
const send = (m) => navigator.sendBeacon('/rum', JSON.stringify(m));
onINP(send); onLCP(send); onCLS(send);

В Performance-панели DevTools ищите длинные задачи (> 50 мс) с жёлтыми блоками performOnConcurrentRoot — это фаза render; фиолетовые Layout/Paint сразу после — коммит оказался тяжёлым для браузера. Разметка performance.measure из React 18 подписывает задачи именами компонентов.

Отдельно: React Compiler (стабилен с 2025 года, ставится как плагин Babel/SWC) автоматически мемоизирует компоненты и значения, анализируя код на чистоту. Он снимает большую часть ручных useMemo и useCallback — но только если ваш код следует правилам React. Ещё один аргумент не спорить со StrictMode.

Чего React не делает

React — библиотека представления. У неё нет мнения о роутинге, запросах, стилях, формах и валидации. Это одновременно сила (можно собрать стек под задачу) и слабость (нужно принять десяток решений до первой строки бизнес-логики).

Честное сравнение: почему не Vue, Svelte, Solid или Angular

Модель React — пересчёт сверху вниз: изменилось состояние, перевызываем поддерево, сравниваем результат. Это делает код предсказуемым (компонент — просто функция) и ценой этого — лишняя работа, которую приходится ограничивать вручную или компилятором.

  • Solid и Svelte 5 используют сигналы: обновление точечно меняет ровно те узлы DOM, которые зависят от значения, без сравнения деревьев. Быстрее и меньше по размеру, ручная мемоизация не нужна. Плата — компонент выполняется один раз, и интуиция «это обычная функция JS» ломается; экосистема и рынок труда несопоставимы по объёму.
  • Vue 3 — компромисс: реактивность на прокси плюс виртуальный DOM с компиляторными подсказками. Отличный DX, официальные роутер и стор из коробки, меньше решений на старте. В корпоративном сегменте вне Азии вакансий заметно меньше.
  • Angular — не библиотека, а платформа: DI, RxJS, схематики, строгая структура. Выигрывает в больших долгоживущих корпоративных системах с ротацией команд, где ценна одинаковость. Порог входа выше, гибкости меньше; с сигналами в v16+ разрыв в производительности закрылся.
  • React выигрывает объёмом экосистемы, зрелостью инструментов (DevTools, Testing Library, RSC), количеством разработчиков на рынке и тем, что почти любая задача уже кем-то решена. Для команды это часто важнее миллисекунд.

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

Типичные ошибки

  • useState для значения, которое выводится из props. Копия рассинхронизируется. Считайте в рендере или используйте key для сброса.
  • Мутация состоянияarr.push, obj.field = x, arr.sort(). React сравнивает ссылки, ре-рендера не будет. Массивы: toSorted, toReversed, with, map, filter.
  • key={index} в изменяемом списке. Состояние и фокус остаются на чужой строке.
  • Побочный эффект в теле компонента — запрос, запись в localStorage, изменение переменной модуля. Ломается в StrictMode и под конкурентным рендером.
  • Компонент, объявленный внутри другого компонента. На каждом рендере это новая функция → новый type → размонтирование поддерева со всем состоянием. Выносите наружу.
  • Чтение count сразу после setCount. Значение из замыкания текущего рендера. Нужна зависимость от предыдущего — функциональный апдейтер.
  • useEffect вместо обработчика события. Отправка формы — это событие, а не синхронизация с внешней системой.
  • Условные и вложенные хуки. Порядок вызова — идентификатор хука; сдвиг ломает всё состояние компонента.
  • Мемоизация без измерения. memo и useCallback везде добавляют сравнения и удерживают ссылки; на быстрых компонентах это чистый убыток. И memo бесполезен, если в props летит литерал: <Chart config={{ theme: 'dark' }} /> — новый объект на каждом рендере.
  • Отключённый StrictMode ради «двойных запросов»: вы просто перестали видеть реальный баг.

Мини-итог

React — это одна идея, доведённая до продакшн-качества: интерфейс есть функция состояния, а синхронизацию с DOM берёт на себя библиотека. JSX компилируется в вызовы jsx(), возвращающие неизменяемые объекты-элементы — дешёвое описание, а не сам UI. Компонент обязан быть чистой функцией, потому что React волен вызвать его дважды или бросить рендер на полпути. Состояние — снимок, привязанный к позиции в дереве, а не к переменной; отсюда и функциональные апдейтеры, и key как способ сбросить компонент. Реконсиляция сводит сравнение деревьев к O(n) двумя эвристиками — «разный тип = новое поддерево» и «ключи задают соответствие», — и почти все загадочные баги списков растут из нарушения второй. Fiber превращает рекурсию в прерываемый цикл и хранит хуки связным списком, что объясняет правила хуков. А тормозит приложение обычно не из-за «лишних рендеров», а из-за размера бандла, состояния, поднятого слишком высоко, и тысяч неотвиртуализированных узлов — и это выясняется профайлером, а не интуицией.

Источники

Что дальше

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

Хуки и паттерны React: useState, useEffect, мемоизация, композиция

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

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

Доска запросов