Создай приложение
на React
с нуля
Полный практический курс: от первого компонента до готового приложения. Каждый урок — теория, живые примеры и квиз.
Что такое React
Виртуальный DOM, зачем React, сравнение с ванильным JS
ОсновыJSX
Синтаксис JSX, выражения, условный рендер, className
СинтаксисКомпоненты и Props
Создание компонентов, передача данных через props, composition
АрхитектураuseState
Хук useState, реактивность, обновление состояния
ХукиuseEffect
Побочные эффекты, lifecycle, зависимости, cleanup
ХукиСписки и ключи
Рендер массивов через .map(), зачем нужен key
РендерингФормы и события
Controlled components, onChange, onSubmit, валидация
ИнтерактивФинальный проект
Собираем полноценный Todo-менеджер из всех концепций
ПроектЧто такое React
React — это библиотека для создания пользовательских интерфейсов. Разберёмся, зачем он нужен и как работает под капотом.
Проблема, которую решает React
Представь, что у тебя есть страница с динамическими данными — например, счётчик лайков. В ванильном JS тебе нужно вручную найти элемент в DOM и обновить его при каждом изменении:
// Ванильный 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-дерева. Когда данные меняются:
React создаёт новый Virtual DOM
Строит новое дерево на основе обновлённых данных — быстро, только в памяти.
Сравнивает со старым (diffing)
Находит разницу между старым и новым деревом — этот процесс называется reconciliation.
Обновляет только изменённые узлы
В реальный DOM попадают только минимально необходимые изменения — это делает React очень быстрым.
React — это не замена HTML. Это способ писать UI компонентами: переиспользуемыми, изолированными кирпичиками интерфейса. Кнопка, карточка, форма — каждый элемент это компонент со своей логикой.
React vs ванильный JS
| Аспект | Ванильный JS | React |
|---|---|---|
| Подход | Императивный — как делать | Декларативный — что отобразить |
| Обновление UI | Вручную через DOM API | Автоматически при изменении state |
| Переиспользование | Сложно, копипаст | Компоненты — изолированные единицы |
| Состояние | Глобальные переменные | useState, локальный state |
| Масштабируемость | Растёт в сложности | Компонентная архитектура |
JSX — HTML в JavaScript
JSX — синтаксический сахар, позволяющий писать HTML-подобный код прямо в JavaScript. Babel преобразует его в вызовы React.createElement().
Что такое JSX
JSX — это расширение синтаксиса JavaScript. Технически, это не HTML — это специальный формат, который транспилируется (через Babel или SWC) в обычный JavaScript:
// Ты пишешь JSX:
const element = (
<h1 className="title">
Привет, {name}!
</h1>
);
// Babel превращает это в:
const element = React.createElement(
'h1',
{ className: 'title' },
'Привет, ', name, '!'
);
Правила JSX
- Одна корневая нода: весь JSX должен быть обёрнут в один элемент (или
<>...</>— Fragment) classNameвместоclass:class— зарезервированное слово в JScamelCaseдля атрибутов:onClick,onChange,tabIndex- Самозакрывающиеся теги:
<img />,<br />,<input /> - Выражения в
{}: любой JS-код, но не операторы (if,for)
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.
className вместо class?class используется для объявления классов ES6, поэтому в JSX его заменяют на className.class — зарезервированное ключевое слово JavaScript. JSX — это JS, поэтому нужен className.Компоненты и Props
Компоненты — это строительные блоки React-приложения. Props (properties) позволяют передавать данные из родителя в дочерний компонент.
Функциональный компонент
В современном React компоненты — это обычные функции JavaScript, которые принимают props и возвращают JSX. Название компонента всегда с заглавной буквы.
// Объявление компонента — функция с заглавной буквы
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. Это делает поведение предсказуемым.
// Деструктуризация 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>
Алексей
Product Analyst
Props текут только сверху вниз: родитель → ребёнок. Если ребёнку нужно уведомить родителя — родитель передаёт функцию-обработчик как prop (например onDelete), и ребёнок её вызывает. Это называется поднятие состояния (lifting state up).
useState.useState — стейт компонента
useState — самый важный хук React. Он позволяет компоненту хранить изменяемые данные и автоматически перерисовываться при их изменении.
Анатомия useState
Хук useState возвращает пару: текущее значение состояния и функцию для его обновления. При вызове функции React перерисовывает компонент с новым значением.
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>
);
}
Важные правила хуков
- Только в теле функции-компонента — не в условиях, не в циклах, не в обычных функциях
- Порядок вызовов постоянный — React отслеживает хуки по порядку вызова между рендерами
- Обновление асинхронное — setCount не меняет count мгновенно, а ставит компонент в очередь на перерендер
- Функциональное обновление — если новое состояние зависит от старого, используй
setCount(prev => prev + 1)
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)?useEffect — побочные эффекты
useEffect позволяет синхронизировать компонент с внешними системами: API, таймерами, подписками. Это замена методам lifecycle в классовых компонентах.
Что такое побочный эффект?
В React чистый рендер — это просто функция, которая возвращает JSX. Всё, что выходит за рамки этого — побочные эффекты: запросы к API, работа с localStorage, подписки на события, таймеры. Для них — 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 |
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>;
}
Списки и ключи
React умеет рендерить массивы JSX-элементов. Для эффективного обновления DOM каждый элемент должен иметь уникальный prop key.
Рендер массива через .map()
Чтобы отобразить список, превращаем массив данных в массив JSX-элементов с помощью .map(). Каждый элемент обязан иметь уникальный key — React использует его для оптимизации обновлений:
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={index} вызывает баги при удалении, добавлении или сортировке элементов: React перепутает компоненты. Используй стабильный ID из данных.
index) как key?Формы и события
В React формы управляются через state — controlled components. Значение поля всегда синхронизировано со state и обновляется через onChange.
Controlled Component
В отличие от HTML, где форма управляет своим состоянием, в React state — единственный источник истины. Значение инпута задаётся через value, а изменения перехватываются через onChange:
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>
);
}
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?e.preventDefault() предотвращает стандартное поведение браузера — перезагрузку страницы при submit формы.Todo-менеджер
Собираем полноценное приложение, используя всё изученное: компоненты, props, useState, useEffect, списки, формы. Это и есть настоящий React.
Архитектура проекта
Разобьём приложение на компоненты. У каждого — одна ответственность:
App — корневой компонент
Хранит весь state: список задач, текст инпута, фильтр. Раздаёт данные и обработчики вниз через props.
AddTaskForm
Форма добавления. Получает onAdd от App и вызывает его при submit. Сам не знает о других задачах.
TaskList + TaskItem
TaskList рендерит массив через .map(). TaskItem отображает одну задачу и вызывает onToggle/onDelete.
useEffect для счётчика
Заголовок страницы document.title обновляется при изменении числа незавершённых задач — побочный эффект.
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-менеджер, собранный здесь, прямо на странице: