React — это JavaScript-библиотека для создания пользовательских интерфейсов из независимых блоков — компонентов. Её создали в Meta (тогда Facebook) в 2013 году, и сегодня на React работают Facebook, Instagram, WhatsApp Web, Netflix, Airbnb, Discord и тысячи личных кабинетов, админок и интернет-магазинов. В этой теме мы разберём, какую проблему решает React, сравним один и тот же код на «чистом» JavaScript и на React и познакомимся с ключевыми идеями, на которых построен весь курс. Устанавливать пока ничего не нужно — проект мы создадим в следующей теме.

Проблема: ручная работа с DOM

Начнём с простого счётчика на ванильном JavaScript. Разметка:

html
<p>Clicks: <span id="count">0</span></p>
<button id="inc">+1</button>
<button id="reset">Reset</button>

И скрипт, который оживляет её:

js
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, и дописать туда обновление второго элемента. Забыли одно место — интерфейс показывает устаревшие данные.

С динамическими списками становится ещё сложнее. Вот список задач с добавлением и удалением:

js
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) мы подробно разберём в следующих темах — сейчас важна сама идея.

jsx
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:

jsx
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):

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):

jsx
function Header({ user }) {
  if (!user) {
    return <button>Log in</button>
  }
  return <p>Welcome back, {user.name}!</p>
}

Во втором варианте невозможно попасть в «сломанное» состояние, когда кнопка входа и приветствие видны одновременно: для каждого значения user описан ровно один результат.

Императивный подходДекларативный подход
Что пишетеШаги: найти, изменить, спрятатьРезультат для каждого состояния
Кто обновляет DOMВы, вручнуюReact
Где правда о данныхРазмазана между переменными и DOMТолько в состоянии (state)
Типичные багиЗабытое обновление, рассинхронРедки: экран всегда следует из данных

Компоненты и однонаправленный поток данных

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

jsx
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>
  )
}

Компоненты образуют дерево:

text
Shop            ← хранит состояние cartCount
├── header      ← показывает cartCount
├── ProductCard ← получает title, price, onAdd через props
└── ProductCard

Данные в React текут в одну сторону — сверху вниз: родитель Shop передаёт карточкам данные через props (свойства компонента). Карточка не может сама изменить cartCount — она лишь вызывает функцию onAdd, которую ей передал родитель, а родитель уже меняет своё состояние. Благодаря этому однонаправленному потоку данных всегда понятно, где хранятся данные и кто их меняет: чтобы найти причину бага, достаточно подняться по дереву вверх.

Виртуальный DOM и reconciliation: интуиция

Когда вы пишете JSX, React не создаёт DOM-элементы сразу. Сначала получается обычный JavaScript-объект — описание того, что нужно показать:

js
// JSX <p className="note">Clicks: {count}</p> при count = 0 превращается в объект:
{
  type: 'p',
  props: { className: 'note', children: ['Clicks: ', 0] },
  key: null,
}

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

  1. Заново вызывает функцию компонента и получает новое дерево объектов.
  2. Сравнивает его с предыдущим деревом — этот процесс называется reconciliation (согласование).
  3. Применяет к настоящему DOM только найденные отличия.
text
Рендер 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-stackNext.js, React Router в режиме фреймворка
Мобильные приложения iOS/AndroidReact Native + Expo
Маршрутизация в SPAReact 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 не знает о ваших изменениях и при следующем рендере перезапишет их.

jsx
// ❌ Плохо: обходим 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 для мобильных приложений.

Практика

  1. Добавьте в ванильный счётчик из первого раздела второе место, где отображается count (например, <h1> с текстом «Clicks: 0»). Сколько строк пришлось изменить? Опишите словами, что нужно было бы изменить в React-версии.
  2. Откройте любой интернет-магазин и нарисуйте в текстовом виде дерево компонентов страницы товара (шапка, галерея, блок цены, отзывы и т. д.). Отметьте, в каком компоненте должно храниться состояние «количество товара в корзине».
  3. Мысленно введите в ванильный список задач строку <b>bold</b>. Что покажет страница? А React-версия? Объясните разницу.
  4. Спроектируйте виджет погоды для города Baku: перечислите возможные состояния (загрузка, ошибка, данные получены), компоненты и их props. Для каждого состояния напишите на «псевдо-JSX», что должно быть на экране.

💡 В задании 4 не нужен рабочий код — важно научиться думать состояниями: «при таких данных экран выглядит так».