React — декларативные интерфейсы

React — самая популярная библиотека для построения пользовательских интерфейсов. Вместо ручного изменения DOM ты описываешь, как выглядит интерфейс при данных, а React сам обновляет страницу при изменении данных.

Уровень: средний 20 разделов ⏱ 4–5 часов

1. Что такое React

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

2. Создание проекта

Современный шаблон — через Vite:

npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
npm run dev
src/
├── 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="Анна" />;
}
JSX — не HTML. Класс пишется как 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>
  );
}
Типизируй props через 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>
  );
}
Контролируемые компоненты удобны для валидации и «живого» превью, но вынуждают на каждый символ вызывать setState. Для крупных форм рассмотри библиотеку форм (например, React Hook 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);
}
Для глобального состояния со сложной логикой подойдёт отдельный стейт-менеджер. В React-экосистеме это Redux Toolkit или Zustand; в Vue — Pinia (курс Pinia).

13. Оптимизация производительности

Компоненты перерендериваются по родителям. Три главных приёма:

// пропустить дорогой рендер, если пропсы не менялись
const MemoCard = React.memo(Card);

// стабилизировать колбэки
const onOpen = useCallback((id) => {
  open(id);
}, [open]);

// мемоизировать вычисления
const total = useMemo(() => computeTotal(items), [items]);

Инструменты измерения:

Не мемоизируй всё подряд: лишние useMemo/useCallback замусоривают память. Оптимизируй по результатам профайлера.

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();
});

Лучшие практики:

Скриншоты и E2E — на Playwright (см. курс Playwright) — покрывают целые сценарии, юниты — точечные функции.

15. StrictMode и аккуратные эффекты

<React.StrictMode> в development дважды вызывает рендер и эффекты — это ловит эффекты с сайд-эффектами (двойные подписки, утечки):

useEffect(() => {
  const conn = socket.connect();
  return () => conn.disconnect();   // обязательно
}, []);

Правила эффектов:

В React 18+ эффекты иногда выполняются дважды в prod-строгом режиме, если нет очистки. StrictMode — друг, а не враг.

16. Паттерны: порталы, композиция, render props

Порталы рисуют JSX в другой DOM-узел — удобно для модалок и тултипов, чтобы не рваться стилями родителя:

const modal = (
  <div className="modal-backdrop">
    <div className="modal">Контент</div>
  </div>
);
return createPortal(modal, document.body);
Паттерн — инструмент на задачу: если компонент без него проще — оставь без него. Простота выигрывает у модности.

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 с кнопкой «Перезагрузить» — пользователь не увидит пустого экрана.

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) });
Не храни форму в глобальном стейте: лишняя синхронизация делает её хрупкой и тормозит каждый ввод.

19. Экосистема и метафреймворки

React — фундамент отличных стеков. Для продакшен-приложений чаще берут метафреймворк, чем «голый» Vite+React:

ИнструментСильные стороны
Next.jsSSR/SSG, маршрутизация по файлам, App Router
Remixформы и прогрессивное улучшение, loader/action
Vite + Reactбыстрый dev, SPA без SSR — для админок
TanStack Queryкэш серверных данных, ретраи, инвалидация
Выбирай стек по задаче: публичный лендинг — Next.js, LOB-приложение — Vite+React. Не усложняй сверх необходимости.

20. Чек-лист продакшена и типичные ошибки

Перед релизом пройди по контрольному списку:

npm run build; npm run lint; npm test
Частая ошибка — «козырь» в useEffect: эффекты запускаются после рендера и в строгом режиме дважды. Логика событий — в обработчиках.

21. Типизация компонентов: 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" };
Держи пропсы плоскими и явными: «магические» объекты усложняют переиспользование и чтение кода через месяц после написания.

22. Оптимизация изображений и медиа

Картинки — главный источник задержки и «прыжков» layout. Минимальный набор практик:

<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"
/>
lazy на первом экране вредит LCP: браузер может увидеть картинку позже. Грузи «над сгибом» сразу, а всё остальное — лениво.

23. Безопасность React-приложений

React экранирует вывод по умолчанию — но дыры появляются там, где «защиту» обходят вручную:

УгрозаРискЗащита
dangerouslySetInnerHTMLвнедрение HTML — XSSочистка DOMpurify, запрет на недоверенный ввод
ссылки из пользовательского вводаjavascript:-URLвалидируй протокол http/https
секреты в клиентском кодетокены и ключи видны в бандлесекреты только на сервере, env без префикса VITE_
устаревшие зависимостиизвестные CVEnpm audit и регулярные обновления
npm audit --production
npm outdated
Content Security Policy режет сам вектор XSS: запрети inline-скрипты, где получится, и оставь источник только для своего бандла.

24. React 19: Actions, useOptimistic и Server Components

React 19 переносит акцент с «обновить состояние» на «выполнить действие». Ключевые новинки:

// useOptimistic: мгновенно показываем результат
const [optimisticCount, addOptimistic] = useOptimistic(count,
  (state, by: number) => state + by);

<form action={addToCart}>
  <button onClick={() => addOptimistic(1)}>Добавить</button>
</form>
Обновляйся постепенно: Actions можно завести в текущем Vite-приложении и без полной миграции на метафреймворк — выигрыш заметен уже на формах.

Что дальше

Добавь к приложению типы из курса TypeScript, сборку ускорим в Vite, стили — в Sass. Тесты компонентов напиши на Jest с Testing Library, а API-клиент разбери в курсе REST API. Перфоманс проверим аудитом Lighthouse.

← Вернуться к курсам