React — декларативные интерфейсы
React — самая популярная библиотека для построения пользовательских интерфейсов. Вместо ручного изменения DOM ты описываешь, как выглядит интерфейс при данных, а React сам обновляет страницу при изменении данных.
1. Что такое React
- Декларативность: ты говоришь «что должно быть», а не «как это сделать»;
- Компонентный подход: интерфейс = дерево переиспользуемых компонентов;
- Виртуальный DOM: React минимальными партиями обновляет настоящий DOM;
- Мощная экосистема: спрос на React-разработчиков стабильно высокий.
2. Создание проекта
Современный шаблон — через Vite:
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
npm run devsrc/
├── main.tsx # точка входа
├── App.tsx # корневой компонент
├── components/ # переиспользуемые компоненты
└── hooks/ # кастомные хуки3. Компоненты и JSX
Компонент — функция, возвращающая JSX (разметку, похожую на HTML):
// Greeting.tsx
export function Greeting({ name }: { name: string }) {
return <h1>Привет, {name}!</h1>;
}// App.tsx
import { Greeting } from "./components/Greeting";
export function App() {
return <Greeting name="Анна" />;
}className, а в атрибутах работают JS-выражения: style={{ color: "red" }}, {isActive ? 1 : 2}.4. Props — передача данных вниз
interface ProductProps {
title: string;
price: number;
inStock: boolean;
}
export function ProductCard({ title, price, inStock }: ProductProps) {
return (
<article>
<h3>{title}</h3>
<p>{price} ₽</p>
{inStock ? <p>В наличии</p> : <p>Нет в наличии</p>}
</article>
);
}interface — подробности в курсе TypeScript. Опечатки в именах ловятся ещё до запуска.5. Состояние: useState
Хук useState хранит состояние и обновляет компонент при изменении:
import { useState } from "react";
export function Counter() {
const [count, setCount] = useState(0);
return (
<p>
Счёт: {count}
<button onClick={() => setCount(count + 1)}>+1</button>
</p>
);
}count++ внутри компонента не перерисует интерфейс. Всегда используй set-функцию и создавай новые объекты/массивы, а не меняй старые.6. Эффекты: useEffect
useEffect выполняет побочные эффекты после рендера (запросы, подписки, таймеры):
import { useEffect, useState } from "react";
export function Products() {
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch("/api/products")
.then(r => r.json())
.then(setItems)
.finally(() => setLoading(false));
}, []); // [] — запустить один раз при монтировании
if (loading) return <p>Загрузка…</p>;
return <ul>{items.map(...)}</ul>;
}// Очистка эффекта: таймеры, подписки, слушатели
useEffect(() => {
const timer = setInterval(tick, 1000);
return () => clearInterval(timer); // cleanup при размонтировании
}, []);7. Списки и ключи
const products = [
{ id: 1, title: "Телефон" },
{ id: 2, title: "Ноутбук" }
];
export function List() {
return (
<ul>
{products.map(p => (
<li key={p.id}>{p.title}</li>
))}
</ul>
);
}key обязателен для элементов в списке. Без него React не сможет корректно переиспользовать узлы, что приводит к багам с состоянием и фокусом. Не используй индекс массива как key, если порядок изменяется.8. Условный рендер
function Status({ error, loading }) {
if (loading) return <Spinner />;
if (error) return <p className="error">{error}</p>;
return <p>Данные загружены</p>;
}
// Или логическое И и тернарник прямо в JSX:
{isAdmin && <button>Админка</button>}
{inStock ? "Есть" : "Нет"}9. Формы (controlled components)
Значение инпута хранится в состоянии — так форма называется «контролируемой»:
import { useState } from "react";
export function LoginForm() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
function submit(e: React.FormEvent) {
e.preventDefault();
console.log({ email, password });
}
return (
<form onSubmit={submit}>
<input value={email} onChange={e => setEmail(e.target.value)}
placeholder="Email" />
<input type="password" value={password}
onChange={e => setPassword(e.target.value)} />
<button type="submit">Войти</button>
</form>
);
}10. Кастомные хуки
Логику повторного использования выносим в хуки с префиксом use:
// hooks/useFetch.ts
import { useEffect, useState } from "react";
export function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(url)
.then(r => r.json())
.then(setData)
.catch(e => setError(e.message))
.finally(() => setLoading(false));
}, [url]);
return { data, error, loading };
}const { data, loading } = useFetch<Product[]>("/api/products");11. React Router
npm install react-router-dom// main.tsx
import { BrowserRouter, Routes, Route } from "react-router-dom";
import Home from "./views/Home";
import Product from "./views/Product";
export function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/product/:id" element={<Product />} />
</Routes>
</BrowserRouter>
);
}// в компоненте Product
import { useParams } from "react-router-dom";
const { id } = useParams();12. Контекст и глобальное состояние
Контекст передаёт данные без прокидывания props через каждую ступень дерева:
// auth.tsx
import { createContext, useContext } from "react";
const AuthContext = createContext<{ user: string | null }>(null!);
export function AuthProvider({ children }) {
const user = localStorage.getItem("user");
return <AuthContext.Provider value={{ user }}>{children}</AuthContext.Provider>;
}
export function useAuth() {
return useContext(AuthContext);
}13. Оптимизация производительности
Компоненты перерендериваются по родителям. Три главных приёма:
// пропустить дорогой рендер, если пропсы не менялись
const MemoCard = React.memo(Card);
// стабилизировать колбэки
const onOpen = useCallback((id) => {
open(id);
}, [open]);
// мемоизировать вычисления
const total = useMemo(() => computeTotal(items), [items]);Инструменты измерения:
- React DevTools → Profiler записывает время рендера;
- ключи списков стабильные — не пересоздавай элементы;
- тяжёлую логику выноси из рендера в эффект или worker.
14. Тестирование компонентов
Юнит-тесты пишут на Vitest + @testing-library/react. Ключевой принцип — «query by role», как пользователь:
import { render, screen, fireEvent } from "@testing-library/react";
test("кнопка открывает модалку", () => {
render(<Header />);
fireEvent.click(screen.getByRole("button", { name: /меню/i }));
expect(screen.getByText("Каталог")).toBeInTheDocument();
});Лучшие практики:
- тестируй поведение, а не внутренности: без вовлечения класса/стиля;
- обёртки для API мокей: у теста нет реальной сети;
- интеракция — fireEvent, ассинхронность — findBy*.
15. StrictMode и аккуратные эффекты
<React.StrictMode> в development дважды вызывает рендер и эффекты — это ловит эффекты с сайд-эффектами (двойные подписки, утечки):
useEffect(() => {
const conn = socket.connect();
return () => conn.disconnect(); // обязательно
}, []);Правила эффектов:
- каждая подписка/таймер очищаются в return cleanup;
- зависимости — реально используемые значения, не «по вкусу»;
- если эффект прыгает между значениями — это кандидат на обработчик события.
16. Паттерны: порталы, композиция, render props
Порталы рисуют JSX в другой DOM-узел — удобно для модалок и тултипов, чтобы не рваться стилями родителя:
const modal = (
<div className="modal-backdrop">
<div className="modal">Контент</div>
</div>
);
return createPortal(modal, document.body);- Композиция — компоненты принимают children и вкладываются друг в друга вместо склейки от переиспользования;
- Render props — движение функции-рендера внутрь компонента для гибких вариаций;
- Container/Presentational разделяет логику и разметку.
17. Ошибки, Suspense и ленивая загрузка
Продакшен требует заранее продуманных «что если»: поломался API, медленно загружается большой экран. Error Boundary ловит ошибки рендера:
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError() { return { hasError: true }; }
render() {
return this.state.hasError
? <div>Что-то сломалось. Обновите страницу.</div>
: this.props.children;
}
}// ленивая загрузка + Suspense
const BigChart = lazy(() => import("./BigChart"));
<Suspense fallback={<Spinner />}>
<BigChart />
</Suspense>- Error Boundary реагирует только на ошибки рендера и lifecycle, не на события;
- ленивая загрузка режет начальный JS-бандл, но добавляет «спиннер»;
- не недофиксируй память: модалки и тяжёлые экраны — лучшие кандидаты на lazy.
18. Формы: React Hook Form и валидация Zod
Формы — главный источник боли. React Hook Form убирает лишние ререндеры и упрощает валидацию:
import { useForm } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
const schema = z.object({
email: z.string().email(),
age: z.coerce.number().min(18),
});
const { register, handleSubmit, formState: { errors } } =
useForm({ resolver: zodResolver(schema) });- register — регистрация поля, значения без useEffect;
- схема Zod единая для UI и (опционально) бэкенда;
- валидация на blur/change, а не на каждый символ при «лентяйном» режиме.
19. Экосистема и метафреймворки
React — фундамент отличных стеков. Для продакшен-приложений чаще берут метафреймворк, чем «голый» Vite+React:
| Инструмент | Сильные стороны |
|---|---|
| Next.js | SSR/SSG, маршрутизация по файлам, App Router |
| Remix | формы и прогрессивное улучшение, loader/action |
| Vite + React | быстрый dev, SPA без SSR — для админок |
| TanStack Query | кэш серверных данных, ретраи, инвалидация |
- SSR важен для SEO и скорости первого экрана;
- для внутренних dashboard хватит Vite+React+TQ;
- сайд эффекты: React Router для навигации, Tailwind/CSS-модули для стилей.
20. Чек-лист продакшена и типичные ошибки
Перед релизом пройди по контрольному списку:
- все списки имеют стабильные key (не index), нет warning'ов в консоли;
NODE_ENV=productionи сборка без sourcemap секретов;- проверка на медленном интернете: lazy-чанки, preload главного;
- доступность: alt, aria-атрибуты для иконок, контраст;
- независимость от мутаций
React.StrictMode— эффекты чисты; - страница с ошибкой — не пустой экран.
npm run build; npm run lint; npm test21. Типизация компонентов: children, события и ComponentProps
TypeScript превращает компоненты в контракт: ошибки видны до запуска приложения. Типы для одной и той же «обёртки» легко переиспользовать:
import type { ReactNode, ChangeEvent, ComponentProps } from "react";
interface CardProps {
title: string;
children: ReactNode;
onSelect?: (id: string) => void;
}
export function Card({ title, children, onSelect }: CardProps) { ... }
// события: узкий тип вместо any
const handleChange = (e: ChangeEvent<HTMLInputElement>) => e.target.value;
// прокинуть пропсы существующего элемента в свой компонент
type ButtonProps = ComponentProps<"button"> & { variant: "primary" | "ghost" };ReactNode— для children: строка, JSX, массив или null;- события типизируются по элементу:
ChangeEvent,MouseEvent,FormEvent; ComponentProps<"tag">распаковывает пропсы элемента без ручного дублирования;- для переопределения пропсов используй
OmitилиExtract.
22. Оптимизация изображений и медиа
Картинки — главный источник задержки и «прыжков» layout. Минимальный набор практик:
- задавай width и height в разметке — браузер резервирует место, и CLS не гуляет;
loading="lazy"иdecoding="async"для изображений ниже первого экрана;- srcset + sizes подбирают файл по ширине вьюпорта;
- критичное изображение первого экрана — preload, а не lazy.
<img
src="/hero-800.webp"
srcSet="/hero-400.webp 400w, /hero-800.webp 800w, /hero-1600.webp 1600w"
sizes="(min-width: 900px) 800px, 100vw"
alt="Главный баннер проекта"
loading="eager"
decoding="async"
width="1600" height="900"
/>23. Безопасность React-приложений
React экранирует вывод по умолчанию — но дыры появляются там, где «защиту» обходят вручную:
| Угроза | Риск | Защита |
|---|---|---|
| dangerouslySetInnerHTML | внедрение HTML — XSS | очистка DOMpurify, запрет на недоверенный ввод |
| ссылки из пользовательского ввода | javascript:-URL | валидируй протокол http/https |
| секреты в клиентском коде | токены и ключи видны в бандле | секреты только на сервере, env без префикса VITE_ |
| устаревшие зависимости | известные CVE | npm audit и регулярные обновления |
npm audit --production
npm outdated24. React 19: Actions, useOptimistic и Server Components
React 19 переносит акцент с «обновить состояние» на «выполнить действие». Ключевые новинки:
- Actions — функции в
<form action>: React сам управляет pending и error-состояниями; - useOptimistic — мгновенный оптимистичный UI с откатом при ошибке;
- use() — чтение промисов и контекста прямо в рендере вместе с Suspense;
- Server Components — рендер тяжёлых секций на сервере без клиентского бандла.
// useOptimistic: мгновенно показываем результат
const [optimisticCount, addOptimistic] = useOptimistic(count,
(state, by: number) => state + by);
<form action={addToCart}>
<button onClick={() => addOptimistic(1)}>Добавить</button>
</form>Что дальше
Добавь к приложению типы из курса TypeScript, сборку ускорим в Vite, стили — в Sass. Тесты компонентов напиши на Jest с Testing Library, а API-клиент разбери в курсе REST API. Перфоманс проверим аудитом Lighthouse.