Веб-разработка
React: основы
FaxJS за одну ночь
Facebook Timeline - один из самых сложных интерфейсов своего времени: реальное время, бесконечный скролл, разные типы контента. jQuery-код стал неуправляемым: изменение одного компонента непредсказуемо ломало другие. Инженер Adam Moss написал "FaxJS" - прототип на основе идеи UI = f(state), где весь интерфейс это функция от данных. В 2013 году React был представлен на JSConf US. Аудитория встретила его скептически: "HTML в JavaScript? Серьёзно?" Через три года React стал стандартом индустрии, а через пять - компонентная модель перекочевала в Vue 3, Angular, Svelte и мобильную разработку (React Native).
Компонентная модель React изменила не только фронтенд - она повлияла на архитектуру мобильных приложений (React Native), десктопных (Electron + React) и серверного рендеринга (Next.js, Remix).
Virtual DOM и компонентная модель
2011 год. Facebook запускает Timeline - и инженер Adam Moss смотрит на 10 000 строк jQuery-спагетти, где изменение одного поля ломает три несвязанных блока. За ночь он пишет "FaxJS" - прототип системы, где UI это функция от состояния: `UI = f(state)`. Через два года Facebook откроет React, и фронтенд никогда не станет прежним.
Центральная идея React - компонент. Не виджет, не модуль, не класс в классическом смысле. Компонент - это функция, которая принимает данные (props) и возвращает описание UI. Ничего больше. Вся сложность приложения - это дерево таких функций.
Но компоненты сами по себе - ещё не революция. Революция - Virtual DOM. Проблема с прямой манипуляцией DOM через `document.querySelector` в том, что каждое изменение заставляет браузер пересчитывать layout, repaint, recomposite. Это дорого. React делает иначе: сначала строит виртуальное дерево объектов в памяти (Virtual DOM), сравнивает его с предыдущим состоянием, и только минимально необходимые изменения отправляет в реальный DOM. Это как диффинг git-коммитов - не перезаписывает весь файл, только изменённые строки.
Однонаправленный поток данных - третий кит React. Данные идут только вниз: от родительского компонента к дочерним через props. Дочерний компонент никогда не изменяет props напрямую - он сообщает об изменении через callback-функцию, переданную сверху. Это кажется ограничением, но на практике делает приложение предсказуемым: в любой момент понятно, откуда пришли данные и кто несёт ответственность за их изменение. Vue 3, Solid, Svelte - все переняли эту модель.
Что делает Virtual DOM в React?
JSX и reconciliation
JSX выглядит как HTML внутри JavaScript - и именно это сбивает с толку большинство новичков. JSX - это не строка, не шаблон и не магия браузера. Это синтаксический сахар над вызовами функций. Babel (или TypeScript) при сборке превращает каждый JSX-тег в вызов `React.createElement()`, который возвращает обычный JavaScript-объект.
Reconciliation - это алгоритм, которым React сравнивает два Virtual DOM дерева. Наивный diff двух деревьев - O(n^3). React использует эвристики и делает это за O(n): если тип узла изменился (например, `div` стал `span`) - удалить и создать заново; если тип тот же - обновить атрибуты. Ключи (`key`) помогают React понять какой элемент в списке соответствует какому при перестановках.
Key - это не prop для компонента. React снимает его до того как передаёт props. Частая ошибка - использовать индекс массива как key: `key={index}`. При перестановке элементов индексы смещаются, React путается и делает ненужные re-render или теряет state. Правило: key из стабильного ID данных, никогда из индекса (если список не статичный).
React 18 ввёл Concurrent Mode и Fiber-архитектуру. Reconciliation теперь прерываемый: React может остановить рендеринг на полпути, заняться более приоритетной задачей (например, анимацией) и вернуться. Это основа useTransition и Suspense.
Почему использование индекса массива как key является проблемой?
Hooks: useState и useEffect
До 2019 года state в React был привилегией классовых компонентов. Функциональный компонент - красивая штука, но без памяти: вызвали, вернул JSX, забыл всё. Hooks изменили это. `useState` даёт функциональному компоненту способность помнить значение между рендерами. Под капотом React хранит hooks в связном списке, привязанном к конкретной позиции компонента в дереве - поэтому нельзя вызывать хуки в условиях или циклах: порядок должен быть детерминированным каждый рендер.
Вызов `setCount` не изменяет переменную немедленно. Он говорит React: "при следующем рендере дай компоненту это новое значение". React батчит несколько вызовов set из одного event handler в один рендер-цикл. Это значит, что если вызвать `setCount(count + 1)` три раза подряд в одном обработчике - счётчик вырастет на 1, а не на 3. Функциональная форма `setCount(prev => prev + 1)` решает эту проблему: она всегда работает с актуальным значением.
`useEffect` - мост между чистым рендером и побочными эффектами: запросы к API, подписки, таймеры, изменения заголовка страницы. Без `useEffect` эти операции либо выполнялись бы при каждом рендере, либо требовали классовых методов жизненного цикла. Хук принимает функцию и массив зависимостей.
Три режима `useEffect` по массиву зависимостей: пустой массив `[]` - выполнить один раз после первого рендера (аналог componentDidMount); конкретные переменные `[dep1, dep2]` - выполнить при монтировании и при изменении зависимостей; без массива вообще - выполнять при каждом рендере (почти всегда ошибка).
React DevTools показывает дерево компонентов и текущий state/props каждого - незаменимо при дебаге. В production-билде React включает дополнительные оптимизации и убирает dev-только предупреждения.
useState обновляет переменную немедленно после вызова set-функции
Вызов set-функции только планирует следующий рендер с новым значением; в текущем рендере переменная остаётся старой
React - это функция `UI = f(state)`. Каждый рендер - это отдельный снимок с замороженными значениями. Если нужно читать новое значение сразу - использовать функциональное обновление `prev => prev + 1` или useReducer.
Что произойдёт, если вызвать `setCount(count + 1)` три раза подряд в одном event handler?
Ключевые идеи
- **Компонент = функция от props:** UI строится как дерево маленьких функций, каждая отвечает за свой фрагмент интерфейса
- **Virtual DOM:** React держит копию UI в памяти JS, диффит её и отправляет в реальный DOM только минимальный патч - дорогие browser reflow только там, где реально нужно
- **JSX компилируется в React.createElement():** никакой магии - просто синтаксический сахар над вызовами функций, проверяемый TypeScript
- **useState планирует рендер, не изменяет немедленно:** каждый рендер - снимок со своими значениями; для накопительных обновлений - функциональная форма `prev => ...`
- **useEffect для побочных эффектов:** fetch, subscriptions, timers - всё вне рендер-функции, с явными зависимостями и cleanup
Связанные темы
React - это слой view. Вокруг него строится экосистема:
- DOM и браузерные API — Virtual DOM абстрагирует прямую работу с реальным DOM
- State Management — Redux, Zustand, Jotai - следующий уровень управления состоянием
- SSR и Next.js — Server Components и SSG строятся поверх компонентной модели React
- Vue и Angular — Альтернативные фреймворки с похожей компонентной моделью, другими trade-offs
Вопросы для размышления
- Если `UI = f(state)` - чистая функция без побочных эффектов, зачем тогда нужен useEffect? Что происходит с этой моделью, когда нужно обратиться к серверу?
- React батчит обновления state из одного event handler. Почему это хорошая идея с точки зрения производительности - и какие сценарии это усложняет?
- Virtual DOM добавляет слой абстракции и неизбежно тратит память. В каких случаях прямая манипуляция DOM всё равно будет быстрее React?
Связанные уроки
- web-03 — JavaScript-основы нужны до React
- web-04 — DOM-модель - фундамент под Virtual DOM
- web-07 — Vue и Angular проще воспринимаются после React
- web-08 — State management - логичное продолжение хуков
- web-09 — Next.js строится поверх компонентной модели React
- aie-05-api-integration — Однонаправленный поток данных - та же идея что однонаправленный pipeline в LLM API
- comp-01-intro