Интерактивный курс · 8 уроков

Создай приложение
на React
с нуля

Полный практический курс: от первого компонента до готового приложения. Каждый урок — теория, живые примеры и квиз.

Урок 01

Что такое React

Виртуальный DOM, зачем React, сравнение с ванильным JS

Основы
Урок 02

JSX

Синтаксис JSX, выражения, условный рендер, className

Синтаксис
Урок 03

Компоненты и Props

Создание компонентов, передача данных через props, composition

Архитектура
Урок 04

useState

Хук useState, реактивность, обновление состояния

Хуки
Урок 05

useEffect

Побочные эффекты, lifecycle, зависимости, cleanup

Хуки
Урок 06

Списки и ключи

Рендер массивов через .map(), зачем нужен key

Рендеринг
Урок 07

Формы и события

Controlled components, onChange, onSubmit, валидация

Интерактив
Урок 08

Финальный проект

Собираем полноценный Todo-менеджер из всех концепций

Проект
Урок 01 · Основы

Что такое React

React — это библиотека для создания пользовательских интерфейсов. Разберёмся, зачем он нужен и как работает под капотом.

Проблема, которую решает React

Представь, что у тебя есть страница с динамическими данными — например, счётчик лайков. В ванильном JS тебе нужно вручную найти элемент в DOM и обновить его при каждом изменении:

JavaScript Ванильный JS — много ручной работы
// Ванильный JS: обновляем DOM вручную
let count = 0;
const btn = document.getElementById('btn');
const display = document.getElementById('count');

btn.addEventListener('click', () => {
  count++;
  display.textContent = count; // ручное обновление DOM
});

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

Виртуальный DOM

React работает через Virtual DOM — легковесную JS-копию реального DOM-дерева. Когда данные меняются:

1

React создаёт новый Virtual DOM

Строит новое дерево на основе обновлённых данных — быстро, только в памяти.

2

Сравнивает со старым (diffing)

Находит разницу между старым и новым деревом — этот процесс называется reconciliation.

3

Обновляет только изменённые узлы

В реальный DOM попадают только минимально необходимые изменения — это делает React очень быстрым.

Ключевая идея

React — это не замена HTML. Это способ писать UI компонентами: переиспользуемыми, изолированными кирпичиками интерфейса. Кнопка, карточка, форма — каждый элемент это компонент со своей логикой.

React vs ванильный JS

АспектВанильный JSReact
ПодходИмперативный — как делатьДекларативный — что отобразить
Обновление UIВручную через DOM APIАвтоматически при изменении state
ПереиспользованиеСложно, копипастКомпоненты — изолированные единицы
СостояниеГлобальные переменныеuseState, локальный state
МасштабируемостьРастёт в сложностиКомпонентная архитектура
Проверь себя
Как называется процесс, при котором React сравнивает старое и новое дерево Virtual DOM?
✓ Правильно! Reconciliation — алгоритм сравнения деревьев для минимизации DOM-операций.
✗ Не совсем. Правильный ответ: Reconciliation — процесс сравнения старого и нового Virtual DOM.
Урок 02 · Синтаксис

JSX — HTML в JavaScript

JSX — синтаксический сахар, позволяющий писать HTML-подобный код прямо в JavaScript. Babel преобразует его в вызовы React.createElement().

Что такое JSX

JSX — это расширение синтаксиса JavaScript. Технически, это не HTML — это специальный формат, который транспилируется (через Babel или SWC) в обычный JavaScript:

JSX Что ты пишешь vs что получает браузер
// Ты пишешь JSX:
const element = (
  <h1 className="title">
    Привет, {name}!
  </h1>
);

// Babel превращает это в:
const element = React.createElement(
  'h1',
  { className: 'title' },
  'Привет, ', name, '!'
);

Правила JSX

  • Одна корневая нода: весь JSX должен быть обёрнут в один элемент (или <>...</> — Fragment)
  • className вместо class: class — зарезервированное слово в JS
  • camelCase для атрибутов: onClick, onChange, tabIndex
  • Самозакрывающиеся теги: <img />, <br />, <input />
  • Выражения в {}: любой JS-код, но не операторы (if, for)
JSX Выражения, условный рендер, списки
function Greeting({ name, isAdmin }) {
  return (
    <div>
      {/* Выражение в {} */}
      <h1>Привет, {name.toUpperCase()}!</h1>

      {/* Условный рендер через && */}
      {isAdmin && <span>Администратор</span>}

      {/* Тернарный оператор */}
      <p>{isAdmin ? 'Полный доступ' : 'Ограниченный доступ'}</p>

      {/* Стили как объект */}
      <div style={{ color: 'teal', fontSize: '16px' }}>
        Цветной текст
      </div>
    </div>
  );
}
Частая ошибка

В JSX нельзя использовать операторы if и for напрямую внутри разметки. Используй тернарный оператор ? :, логическое &&, или выноси логику в переменную перед return.

Живой пример: JSX и выражения
Введи имя выше чтобы увидеть результат...
Проверь себя
Почему в JSX используется className вместо class?
✓ Верно! В JS class используется для объявления классов ES6, поэтому в JSX его заменяют на className.
✗ Правильный ответ: class — зарезервированное ключевое слово JavaScript. JSX — это JS, поэтому нужен className.
Урок 03 · Архитектура

Компоненты и Props

Компоненты — это строительные блоки React-приложения. Props (properties) позволяют передавать данные из родителя в дочерний компонент.

Функциональный компонент

В современном React компоненты — это обычные функции JavaScript, которые принимают props и возвращают JSX. Название компонента всегда с заглавной буквы.

JSX Простой компонент UserCard
// Объявление компонента — функция с заглавной буквы
function UserCard({ name, role, avatar }) {
  return (
    <div className="card">
      <img src={avatar} alt={name} />
      <h3>{name}</h3>
      <p>{role}</p>
    </div>
  );
}

// Использование — как HTML-тег с атрибутами
function App() {
  return (
    <div>
      <UserCard
        name="Алексей"
        role="Product Analyst"
        avatar="/avatar.jpg"
      />
      <UserCard name="Мария" role="Designer" avatar="/m.jpg" />
    </div>
  );
}

Props — данные сверху вниз

Props передаются от родителя к потомку (однонаправленный поток данных). Компонент не может изменить свои props — они read-only. Это делает поведение предсказуемым.

JSX Props и значения по умолчанию
// Деструктуризация props + значения по умолчанию
function Button({
  children,
  variant = 'primary',
  onClick,
  disabled = false
}) {
  return (
    <button
      className={`btn btn-${variant}`}
      onClick={onClick}
      disabled={disabled}
    >
      {children}
    </button>
  );
}

// children — специальный prop, содержимое тега
<Button onClick={handleClick}>Нажми меня</Button>
<Button variant="ghost" disabled>Недоступно</Button>
Живой пример: компонент UserCard с props

Алексей

Product Analyst

Золотое правило

Props текут только сверху вниз: родитель → ребёнок. Если ребёнку нужно уведомить родителя — родитель передаёт функцию-обработчик как prop (например onDelete), и ребёнок её вызывает. Это называется поднятие состояния (lifting state up).

Проверь себя
Может ли компонент изменить свои props напрямую?
✓ Правильно! Props неизменяемы — компонент только читает их. Для изменяемых данных нужен useState.
✗ Нет. Props — read-only. Для хранения изменяемых данных используй useState.
Урок 04 · Хуки

useState — стейт компонента

useState — самый важный хук React. Он позволяет компоненту хранить изменяемые данные и автоматически перерисовываться при их изменении.

Анатомия useState

Хук useState возвращает пару: текущее значение состояния и функцию для его обновления. При вызове функции React перерисовывает компонент с новым значением.

JSX Счётчик с useState
import { useState } from 'react';

function Counter() {
  // [текущее значение, функция обновления] = useState(начальное значение)
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Счёт: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        +1
      </button>
      <button onClick={() => setCount(0)}>
        Сброс
      </button>
    </div>
  );
}
Живой пример: счётчик
0
count = 0

Важные правила хуков

  • Только в теле функции-компонента — не в условиях, не в циклах, не в обычных функциях
  • Порядок вызовов постоянный — React отслеживает хуки по порядку вызова между рендерами
  • Обновление асинхронное — setCount не меняет count мгновенно, а ставит компонент в очередь на перерендер
  • Функциональное обновление — если новое состояние зависит от старого, используй setCount(prev => prev + 1)
JSX Несколько состояний и объект
function UserProfile() {
  // Несколько независимых состояний
  const [name, setName] = useState('');
  const [age, setAge] = useState(0);
  const [isActive, setIsActive] = useState(false);

  // Функциональное обновление (безопасный способ)
  const increment = () => {
    setAge(prev => prev + 1); // prev — гарантированно актуальное значение
  };

  // Состояние-объект: нужно копировать через spread
  const [form, setForm] = useState({ email: '', password: '' });

  const handleEmail = (e) => {
    setForm({ ...form, email: e.target.value }); // spread!
  };
}
Проверь себя
Что произойдёт если написать count = count + 1 вместо setCount(count + 1)?
✓ Верно! React отслеживает изменения только через setter-функцию. Прямое присвоение не триггерит перерендер.
✗ Правильный ответ: React не узнает об изменении переменной — перерендера не будет, UI останется старым.
Урок 05 · Хуки

useEffect — побочные эффекты

useEffect позволяет синхронизировать компонент с внешними системами: API, таймерами, подписками. Это замена методам lifecycle в классовых компонентах.

Что такое побочный эффект?

В React чистый рендер — это просто функция, которая возвращает JSX. Всё, что выходит за рамки этого — побочные эффекты: запросы к API, работа с localStorage, подписки на события, таймеры. Для них — useEffect.

JSX Структура useEffect
import { useState, useEffect } from 'react';

function Timer() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    // Эффект: запускается после рендера
    const interval = setInterval(() => {
      setSeconds(s => s + 1);
    }, 1000);

    // Cleanup: вызывается перед следующим эффектом или unmount
    return () => clearInterval(interval);

  }, []); // [] — пустой массив зависимостей: запустить один раз

  return <p>Прошло: {seconds}с</p>;
}

Массив зависимостей

Второй аргумент useEffect контролирует когда запускается эффект:

ЗависимостиКогда запускаетсяПример использования
Не переданПосле каждого рендераРедко нужно
[] (пустой)Один раз после монтированияЗапрос к API при загрузке
[id, filter]При изменении id или filterПерезагрузка данных при смене ID
JSX Запрос к API и cleanup
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    let cancelled = false; // защита от race condition

    setLoading(true);
    fetch(`/api/users/${userId}`)
      .then(r => r.json())
      .then(data => {
        if (!cancelled) {
          setUser(data);
          setLoading(false);
        }
      });

    return () => { cancelled = true; };
  }, [userId]); // перезапрашиваем при смене userId

  if (loading) return <p>Загрузка...</p>;
  return <h1>{user.name}</h1>;
}
Живой пример: таймер с useEffect
00:00
isRunning = false
Проверь себя
Зачем из useEffect возвращать функцию cleanup?
✓ Верно! Cleanup предотвращает утечки памяти: очищает интервалы, отписывается от событий, отменяет запросы.
✗ Правильный ответ: Cleanup функция вызывается при unmount компонента (и перед следующим запуском эффекта), чтобы очистить подписки и таймеры.
Урок 06 · Рендеринг

Списки и ключи

React умеет рендерить массивы JSX-элементов. Для эффективного обновления DOM каждый элемент должен иметь уникальный prop key.

Рендер массива через .map()

Чтобы отобразить список, превращаем массив данных в массив JSX-элементов с помощью .map(). Каждый элемент обязан иметь уникальный key — React использует его для оптимизации обновлений:

JSX Рендер списка задач
const tasks = [
  { id: 1, text: 'Изучить React', done: false },
  { id: 2, text: 'Написать компонент', done: true },
  { id: 3, text: 'Задеплоить проект', done: false },
];

function TaskList() {
  return (
    <ul>
      {tasks.map((task) => (
        {/* key — обязателен! Используй id, не индекс */}
        <TaskItem
          key={task.id}
          text={task.text}
          done={task.done}
        />
      ))}
    </ul>
  );
}

function TaskItem({ text, done }) {
  return (
    <li style={{ textDecoration: done ? 'line-through' : 'none' }}>
      {text}
    </li>
  );
}
Не используй индекс массива как key

Использование key={index} вызывает баги при удалении, добавлении или сортировке элементов: React перепутает компоненты. Используй стабильный ID из данных.

Живой пример: динамический список с отметками
items.length = 3
Проверь себя
Почему нежелательно использовать индекс массива (index) как key?
✓ Правильно! Если список меняется (удаление/сортировка), индексы «плывут» — компоненты получают чужие данные. Используй стабильный ID.
✗ Правильный ответ: при изменении порядка элементов индексы смещаются. React, ориентируясь на key, неправильно сопоставит старые компоненты с новыми данными.
Урок 07 · Интерактив

Формы и события

В React формы управляются через state — controlled components. Значение поля всегда синхронизировано со state и обновляется через onChange.

Controlled Component

В отличие от HTML, где форма управляет своим состоянием, в React state — единственный источник истины. Значение инпута задаётся через value, а изменения перехватываются через onChange:

JSX Управляемый инпут
function SearchForm() {
  const [query, setQuery] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault(); // предотвращаем перезагрузку страницы
    console.log('Поиск:', query);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={query}         {/* React контролирует значение */}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Поиск..."
      />
      <button type="submit">Найти</button>
    </form>
  );
}
JSX Форма регистрации с валидацией
function RegisterForm() {
  const [form, setForm] = useState({ name: '', email: '' });
  const [errors, setErrors] = useState({});

  // Универсальный обработчик — name атрибут инпута = ключ в state
  const handleChange = (e) => {
    const { name, value } = e.target;
    setForm(prev => ({ ...prev, [name]: value }));
  };

  const validate = () => {
    const errs = {};
    if (!form.name) errs.name = 'Имя обязательно';
    if (!form.email.includes('@')) errs.email = 'Некорректный email';
    return errs;
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    const errs = validate();
    if (Object.keys(errs).length) { setErrors(errs); return; }
    console.log('Отправлено:', form);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="name" value={form.name} onChange={handleChange} />
      {errors.name && <span>{errors.name}</span>}
      <input name="email" value={form.email} onChange={handleChange} />
      {errors.email && <span>{errors.email}</span>}
      <button>Зарегистрироваться</button>
    </form>
  );
}
Живой пример: управляемая форма
Начни вводить — здесь появится текущее состояние формы...
Проверь себя
Зачем вызывать e.preventDefault() в обработчике onSubmit?
✓ Правильно! По умолчанию отправка HTML-формы перезагружает страницу. В React нам это не нужно — мы обрабатываем данные сами.
✗ Правильный ответ: e.preventDefault() предотвращает стандартное поведение браузера — перезагрузку страницы при submit формы.
Урок 08 · Финальный проект

Todo-менеджер

Собираем полноценное приложение, используя всё изученное: компоненты, props, useState, useEffect, списки, формы. Это и есть настоящий React.

Архитектура проекта

Разобьём приложение на компоненты. У каждого — одна ответственность:

App

App — корневой компонент

Хранит весь state: список задач, текст инпута, фильтр. Раздаёт данные и обработчики вниз через props.

Add

AddTaskForm

Форма добавления. Получает onAdd от App и вызывает его при submit. Сам не знает о других задачах.

List

TaskList + TaskItem

TaskList рендерит массив через .map(). TaskItem отображает одну задачу и вызывает onToggle/onDelete.

FX

useEffect для счётчика

Заголовок страницы document.title обновляется при изменении числа незавершённых задач — побочный эффект.

JSX App.jsx — финальное приложение
import { useState, useEffect } from 'react';

function App() {
  const [tasks, setTasks] = useState([
    { id: 1, text: 'Изучить React', done: false },
  ]);
  const [filter, setFilter] = useState('all');

  // useEffect: обновляем title при изменении задач
  useEffect(() => {
    const pending = tasks.filter(t => !t.done).length;
    document.title = pending > 0
      ? `(${pending}) Todo App`
      : 'Todo App';
  }, [tasks]);

  const addTask = (text) => {
    setTasks([...tasks, { id: Date.now(), text, done: false }]);
  };

  const toggleTask = (id) => {
    setTasks(tasks.map(t =>
      t.id === id ? { ...t, done: !t.done } : t
    ));
  };

  const deleteTask = (id) => {
    setTasks(tasks.filter(t => t.id !== id));
  };

  const visible = tasks.filter(t =>
    filter === 'active' ? !t.done :
    filter === 'done'   ? t.done  : true
  );

  return (
    <div>
      <AddTaskForm onAdd={addTask} />
      <FilterBar filter={filter} onFilter={setFilter} />
      <TaskList
        tasks={visible}
        onToggle={toggleTask}
        onDelete={deleteTask}
      />
    </div>
  );
}

Живое приложение

Вот финальный результат — полностью работающий Todo-менеджер, собранный здесь, прямо на странице:

Todo App — финальный проект
0 задач выполнено из 0