React — это JavaScript-библиотека для создания пользовательских интерфейсов из независимых блоков — компонентов. Её создали в Meta (тогда Facebook) в 2013 году, и сегодня на React работают Facebook, Instagram, WhatsApp Web, Netflix, Airbnb, Discord и тысячи личных кабинетов, админок и интернет-магазинов. В этой теме мы разберём, какую проблему решает React, сравним один и тот же код на «чистом» JavaScript и на React и познакомимся с ключевыми идеями, на которых построен весь курс. Устанавливать пока ничего не нужно — проект мы создадим в следующей теме.
Проблема: ручная работа с DOM
Начнём с простого счётчика на ванильном JavaScript. Разметка:
<p>Clicks: <span id="count">0</span></p>
<button id="inc">+1</button>
<button id="reset">Reset</button>И скрипт, который оживляет её:
let count = 0
const countEl = document.querySelector('#count')
const incBtn = document.querySelector('#inc')
const resetBtn = document.querySelector('#reset')
incBtn.addEventListener('click', () => {
count++
countEl.textContent = count // не забыть обновить DOM!
})
resetBtn.addEventListener('click', () => {
count = 0
countEl.textContent = count // и здесь тоже
})Код работает, но обратите внимание: данные (count) и их отображение (countEl.textContent) живут отдельно, и синхронизировать их приходится вручную в каждом обработчике. Если завтра значение счётчика нужно показать ещё и в шапке сайта, придётся найти все места, где меняется count, и дописать туда обновление второго элемента. Забыли одно место — интерфейс показывает устаревшие данные.
С динамическими списками становится ещё сложнее. Вот список задач с добавлением и удалением:
const tasks = ['Learn JSX', 'Build a counter']
const list = document.querySelector('#tasks')
const form = document.querySelector('#task-form')
const input = document.querySelector('#new-task')
function renderTasks() {
list.innerHTML = tasks
.map((task, i) => `<li>${task} <button data-index="${i}">×</button></li>`)
.join('')
}
form.addEventListener('submit', (e) => {
e.preventDefault()
tasks.push(input.value)
input.value = ''
renderTasks() // после каждого изменения — перерисовать вручную
})
list.addEventListener('click', (e) => {
if (e.target.matches('button')) {
tasks.splice(Number(e.target.dataset.index), 1)
renderTasks()
}
})
renderTasks()У такого подхода сразу несколько проблем:
- Всё перерисовывается целиком.
innerHTMLудаляет и заново создаёт все<li>: на длинных списках это медленно, а если внутри были поля ввода, они теряют фокус и набранный текст. - Уязвимость XSS. Если пользователь введёт
<img src=x onerror="alert('hacked')">, строка попадёт вinnerHTMLкак настоящий HTML и скрипт выполнится. - Легко забыть обновление. Каждое изменение данных обязано заканчиваться вызовом
renderTasks(). - Логика размазана. Разметка в HTML-файле, шаблон строки в JS, обработчики через делегирование и
data-index— чтобы понять, как работает интерфейс, нужно держать в голове все три места.
Тот же код на React
Теперь посмотрим на счётчик в React. Синтаксис (JSX, useState) мы подробно разберём в следующих темах — сейчас важна сама идея.
import { useState } from 'react'
function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<p>Clicks: {count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
<button onClick={() => setCount(0)}>Reset</button>
</div>
)
}Здесь нет ни одного querySelector и ни одного textContent. Мы описали, как должен выглядеть интерфейс для любого значения count, а при нажатии кнопки только меняем данные через setCount. Всё остальное React делает сам: вызывает функцию Counter заново, сравнивает результат с предыдущим и обновляет в DOM только текст внутри <p>. Если значение понадобится ещё в шапке, достаточно написать {count} в нужном месте разметки — обновление произойдёт автоматически.
Список задач на React:
import { useState } from 'react'
function TaskList() {
const [tasks, setTasks] = useState([
{ id: 1, title: 'Learn JSX' },
{ id: 2, title: 'Build a counter' },
])
const [text, setText] = useState('')
function handleSubmit(e) {
e.preventDefault()
setTasks([...tasks, { id: crypto.randomUUID(), title: text }])
setText('')
}
return (
<form onSubmit={handleSubmit}>
<input value={text} onChange={(e) => setText(e.target.value)} />
<button>Add</button>
<ul>
{tasks.map((task) => (
<li key={task.id}>
{task.title}{' '}
<button type="button" onClick={() => setTasks(tasks.filter((t) => t.id !== task.id))}>
×
</button>
</li>
))}
</ul>
</form>
)
}Разметка, данные и обработчики теперь собраны в одном месте. При добавлении задачи React создаст в DOM только один новый <li>, остальные элементы не тронет. А строка <img src=x onerror=...> будет показана как обычный текст: React экранирует всё, что выводится через {}, поэтому этот класс XSS-атак исключён по умолчанию.
Декларативный UI: UI = f(state)
Главная идея React укладывается в формулу UI = f(state): интерфейс — это функция от состояния. Вы не пишете инструкции «найди элемент, спрячь кнопку, покажи приветствие», а описываете, что должно быть на экране при каждом возможном состоянии. Такой стиль называется декларативным, а ручные команды DOM — императивным.
Императивно (ванильный JS):
function updateHeader(user) {
if (user) {
loginBtn.hidden = true
greeting.textContent = `Welcome back, ${user.name}!`
greeting.hidden = false
} else {
loginBtn.hidden = false
greeting.hidden = true
}
}Декларативно (React):
function Header({ user }) {
if (!user) {
return <button>Log in</button>
}
return <p>Welcome back, {user.name}!</p>
}Во втором варианте невозможно попасть в «сломанное» состояние, когда кнопка входа и приветствие видны одновременно: для каждого значения user описан ровно один результат.
| Императивный подход | Декларативный подход | |
|---|---|---|
| Что пишете | Шаги: найти, изменить, спрятать | Результат для каждого состояния |
| Кто обновляет DOM | Вы, вручную | React |
| Где правда о данных | Размазана между переменными и DOM | Только в состоянии (state) |
| Типичные баги | Забытое обновление, рассинхрон | Редки: экран всегда следует из данных |
Компоненты и однонаправленный поток данных
Компонент — это функция, которая возвращает кусок интерфейса. Из маленьких компонентов собираются большие, как из деталей конструктора: кнопка → карточка товара → каталог → страница. Один и тот же компонент можно использовать много раз с разными данными.
import { useState } from 'react'
function ProductCard({ title, price, onAdd }) {
return (
<article className="card">
<h3>{title}</h3>
<p>{price} AZN</p>
<button onClick={onAdd}>Add to cart</button>
</article>
)
}
function Shop() {
const [cartCount, setCartCount] = useState(0)
return (
<main>
<header>Cart: {cartCount}</header>
<ProductCard title="Laptop" price={1899} onAdd={() => setCartCount(cartCount + 1)} />
<ProductCard title="Mouse" price={35} onAdd={() => setCartCount(cartCount + 1)} />
</main>
)
}Компоненты образуют дерево:
Shop ← хранит состояние cartCount
├── header ← показывает cartCount
├── ProductCard ← получает title, price, onAdd через props
└── ProductCardДанные в React текут в одну сторону — сверху вниз: родитель Shop передаёт карточкам данные через props (свойства компонента). Карточка не может сама изменить cartCount — она лишь вызывает функцию onAdd, которую ей передал родитель, а родитель уже меняет своё состояние. Благодаря этому однонаправленному потоку данных всегда понятно, где хранятся данные и кто их меняет: чтобы найти причину бага, достаточно подняться по дереву вверх.
Виртуальный DOM и reconciliation: интуиция
Когда вы пишете JSX, React не создаёт DOM-элементы сразу. Сначала получается обычный JavaScript-объект — описание того, что нужно показать:
// JSX <p className="note">Clicks: {count}</p> при count = 0 превращается в объект:
{
type: 'p',
props: { className: 'note', children: ['Clicks: ', 0] },
key: null,
}Дерево таких объектов часто называют виртуальным DOM. Создавать объекты очень дёшево, а вот менять настоящий DOM — дорого: браузеру приходится пересчитывать стили, раскладку и перерисовывать страницу. Поэтому при изменении состояния React работает так:
- Заново вызывает функцию компонента и получает новое дерево объектов.
- Сравнивает его с предыдущим деревом — этот процесс называется reconciliation (согласование).
- Применяет к настоящему DOM только найденные отличия.
Рендер 1 (count = 0) Рендер 2 (count = 1)
<div> <div>
<p>Clicks: 0</p> <p>Clicks: 1</p> ← изменился только текст
<button>+1</button> <button>+1</button>
</div> </div>В DOM поменяется один текстовый узел, кнопки останутся теми же элементами — с тем же фокусом и теми же обработчиками. Подробно этапы рендера и коммита мы разберём в теме «Как React обновляет экран».
💡 Главная ценность виртуального DOM — не «скорость любой ценой», а простая модель программирования: вы каждый раз описываете интерфейс целиком, а React берёт на себя минимальные изменения DOM.
Экосистема React в 2026 году
React — это библиотека именно для интерфейса. Маршрутизация, загрузка данных, формы и сборка решаются отдельными инструментами, и за годы вокруг React сложилась огромная экосистема. Актуальная версия — React 19 (19.3 на момент написания курса): функциональные компоненты, хуки, Actions для форм, стабильный React Compiler, который автоматически оптимизирует повторные рендеры.
| Задача | Чем решают в 2026 году |
|---|---|
| SPA: личный кабинет, админка, дашборд | React + Vite (этот курс) |
| Сайт с SEO и серверным рендерингом, full-stack | Next.js, React Router в режиме фреймворка |
| Мобильные приложения iOS/Android | React Native + Expo |
| Маршрутизация в SPA | React Router, TanStack Router |
| Серверные данные и кэш | TanStack Query |
| Глобальное состояние | Zustand, Redux Toolkit |
| Формы и валидация | React Hook Form + Zod |
| Тесты | Vitest + React Testing Library |
Почти всё из этой таблицы вы освоите в курсе. Знание React открывает дорогу и в Next.js, и в мобильную разработку на React Native — принципы компонентов, props и состояния там те же.
Типичные ошибки
1. Менять DOM вручную внутри React-компонента. React не знает о ваших изменениях и при следующем рендере перезапишет их.
// ❌ Плохо: обходим React
function handleClick() {
document.querySelector('#count').textContent = count + 1
}
// ✅ Хорошо: меняем данные, React обновит DOM сам
function handleClick() {
setCount(count + 1)
}2. Хранить данные в DOM и читать их оттуда. Код вида Number(countEl.textContent) + 1 делает DOM источником правды. В React источник правды — состояние, а DOM лишь отображает его.
3. Ожидать, что обычная переменная обновит экран. Если объявить let count = 0 и в обработчике написать count++, значение изменится, но React не узнает об этом и не перерисует компонент. Для данных, меняющихся со временем, нужен useState — его мы подробно изучим в разделе о состоянии.
4. Считать React фреймворком, где «всё уже есть». В React нет встроенного роутера, HTTP-клиента или валидации форм. Это не недостаток, а осознанный выбор: вы подключаете проверенные библиотеки под задачу.
5. Начинать React без уверенного JavaScript. Большинство «проблем с React» у новичков — на самом деле пробелы в JS. Перед курсом убедитесь, что уверенно владеете стрелочными функциями, деструктуризацией, spread-оператором, методами массивов map/filter, модулями import/export, шаблонными строками, тернарным оператором и async/await.
Итог
- React — библиотека для построения интерфейсов из компонентов; маршрутизацию, данные и формы решают отдельные библиотеки экосистемы.
- Ручная работа с DOM требует синхронизировать данные и экран в каждом обработчике — отсюда рассинхрон, лишние перерисовки и XSS через
innerHTML. - React декларативен: UI = f(state). Вы описываете результат для каждого состояния, а React сам обновляет DOM.
- Компоненты — функции, возвращающие разметку; из них собирается дерево интерфейса.
- Данные текут сверху вниз через props, а наверх передаются вызовы функций-обработчиков.
- Виртуальный DOM и reconciliation позволяют менять в настоящем DOM только то, что действительно изменилось.
- Актуальный стек 2026 года: React 19, Vite для SPA, Next.js и React Router для full-stack, React Native для мобильных приложений.
Практика
- Добавьте в ванильный счётчик из первого раздела второе место, где отображается
count(например,<h1>с текстом «Clicks: 0»). Сколько строк пришлось изменить? Опишите словами, что нужно было бы изменить в React-версии. - Откройте любой интернет-магазин и нарисуйте в текстовом виде дерево компонентов страницы товара (шапка, галерея, блок цены, отзывы и т. д.). Отметьте, в каком компоненте должно храниться состояние «количество товара в корзине».
- Мысленно введите в ванильный список задач строку
<b>bold</b>. Что покажет страница? А React-версия? Объясните разницу. - Спроектируйте виджет погоды для города Baku: перечислите возможные состояния (загрузка, ошибка, данные получены), компоненты и их props. Для каждого состояния напишите на «псевдо-JSX», что должно быть на экране.
💡 В задании 4 не нужен рабочий код — важно научиться думать состояниями: «при таких данных экран выглядит так».