- Полное руководство по React: От Нуля до Архитектора
- Часть 1: Фундамент (The Foundation)
- Глава 1. Подготовка и Modern JavaScript
- 1.1. ES6+ Cheatsheet: Синтаксис, необходимый для React
- 1.2. Асинхронность: Promises, Async/Await и Event Loop
- 1.3. Концепция неизменяемости (Immutability) и ссылочные типы данных
- 1.4. Инструментарий: Node.js и Пакетные менеджеры
- 1.5. Сборщики: Почему Vite — новый король
- Глава 2: Введение в React
- Глава 3: Основы построения UI
- Глава 1. Подготовка и Modern JavaScript
- Часть 2: Стилизация и Визуал
- Часть 3: Оживление приложения
- Часть 4: Управление данными и Архитектура (Data Architecture)
- Часть 5: Качество кода и Профессиональная разработка
- Часть 6: Продвинутые паттерны и Оптимизация
- Часть 7: Современный React и SSR (The Modern Era)
- Часть 8: Enterprise Solutions & Scale
- Эпилог: Карьера и развитие
- Часть 1: Фундамент (The Foundation)
Забудьте про var. В современном React (и JS в целом) мы используем const по умолчанию и let, только если планируем переприсваивать значение.
const: Создает константу в рамках блока. Важно: для объектов и массивов это не означает полную заморозку (содержимое менять можно, ссылку — нет).let: Создает переменную с блочной областью видимости.
// ❌ Old School (var всплывает, не имеет блочной видимости)
var name = "Dmitry";
// ✅ Modern Way
const appName = "MyReactApp"; // Ссылку менять нельзя
let score = 0; // Значение будет меняться
if (true) {
const appName = "AnotherApp"; // Это новая переменная внутри блока, она не конфликтует с внешней
console.log(appName); // "AnotherApp"
}
console.log(appName); // "MyReactApp"В React функциональные компоненты часто записываются через стрелочные функции. Они лаконичнее и не создают свой собственный контекст this (что критично важно, если вы пишете методы внутри классов или колбэки).
// Обычная функция
function sum(a, b) {
return a + b;
}
// ✅ Стрелочная функция
const sumArrow = (a, b) => {
return a + b;
};
// ✅ Если действие в одну строку, return и фигурные скобки можно опустить (Implicit Return)
// Очень часто используется в методах .map() для рендеринга списков
const multiply = (a, b) => a * b;
// ✅ Если аргумент один, скобки вокруг него можно опустить (но Prettier часто возвращает их обратно)
const square = x => x * x;Это, пожалуй, самый используемый паттерн в React. Мы постоянно достаем данные из объектов (Props) и массивов (Hooks).
Деструктуризация объектов:
const user = {
name: "Alex",
age: 25,
role: "Admin",
details: {
city: "Moscow"
}
};
// ❌ Как мы делали раньше
const name = user.name;
const userAge = user.age;
// ✅ Деструктуризация
// Мы создаем переменные name и role, вытаскивая их из объекта user
const { name, role } = user;
// ✅ Можно переименовывать переменные на лету (алиасы)
const { age: userAge } = user; // user.age запишется в переменную userAge
// ✅ Можно задавать значения по умолчанию (если поля нет в объекте)
const { status = "Active" } = user;
// ✅ Глубокая деструктуризация (используется реже, но полезно знать)
const { details: { city } } = user;
// 🔥 Применение в React (Props):
// Вместо (props) => <h1>{props.title}</h1>
// Мы пишем ({ title }) => <h1>{title}</h1>Деструктуризация массивов:
Критически важна для хука useState.
const coordinates = [55.75, 37.61];
// ✅ Берем элементы по порядку. Имена переменных мы придумываем сами!
const [lat, lng] = coordinates;
// 🔥 Применение в React:
// const [count, setCount] = useState(0);
// useState возвращает массив из двух элементов: значения и функции.Три точки ... — мощнейший инструмент. В React он используется для соблюдения иммутабельности (создания копий объектов с изменениями).
Spread (Разворачивание):
const oldUser = { name: "Ivan", age: 30 };
const additionalInfo = { city: "Berlin" };
// ✅ Объединение объектов
const fullUser = { ...oldUser, ...additionalInfo };
// ✅ Обновление свойства (Immutable update pattern)
// Мы создаем НОВЫЙ объект, копируем все поля из oldUser, но перезаписываем age
// React увидит новую ссылку и поймет, что нужно перерисовать компонент
const updatedUser = { ...oldUser, age: 31 };
// То же самое с массивами
const numbers = [1, 2, 3];
const newNumbers = [...numbers, 4, 5]; // [1, 2, 3, 4, 5]Rest (Сбор оставшегося):
const fruit = { name: "Apple", color: "Red", price: 100, weight: 200 };
// Мы достаем name, а ВСЕ ОСТАЛЬНОЕ собираем в объект rest
const { name, ...rest } = fruit;
console.log(rest); // { color: "Red", price: 100, weight: 200 }
// 🔥 Применение в React:
// Часто используется для проброса пропсов дальше
// const Button = ({ label, ...props }) => <button {...props}>{label}</button>Используются для формирования строк с переменными. В React часто нужны для формирования классов CSS или URL.
const fileName = "report";
const ext = "pdf";
// ❌ Old
const fullName = "File: " + fileName + "." + ext;
// ✅ Modern (Backticks ` `)
const fullNameModern = `File: ${fileName}.${ext}`;
// Внутри ${} может быть любое JS выражение
const greeting = `Hello, ${user.isAdmin ? "Admin" : "Guest"}`;React-приложение состоит из сотен файлов. Система модулей ES6 позволяет их связывать.
Named Export (Именованный экспорт): Используется, когда из файла нужно экспортировать несколько сущностей (утилитные функции, константы).
// utils.js
export const sum = (a, b) => a + b;
export const PI = 3.14;
// App.js
import { sum, PI } from './utils'; // Названия должны совпадать!Default Export (Экспорт по умолчанию): Обычно используется для экспорта одного главного компонента из файла.
// Header.js
const Header = () => <div>Logo</div>;
export default Header;
// App.js
import AnyNameHeader from './Header'; // Можно назвать как угодно при импорте, но принято так жеДля вас, как для Java-разработчика, это, возможно, самая важная ментальная смена парадигмы. В Java многопоточность — это реальные потоки ОС. В JavaScript (в браузере) поток один. Если вы заблокируете его "тяжелым" вычислением или синхронным ожиданием ответа от сервера — интерфейс браузера "замерзнет" и перестанет реагировать на клики.
Чтобы этого избежать, JS использует Event Loop (цикл событий) и асинхронность.
До появления async/await (и после "callback hell") стандартом стали Промисы.
Promise — это объект-обертка, который представляет собой значение, которое появится в будущем (или ошибку).
У Промиса есть 3 состояния:
- Pending (Ожидание): Запрос отправлен, ждем.
- Fulfilled / Resolved (Выполнено): Успех, данные получены.
- Rejected (Отклонено): Ошибка (сети, сервера и т.д.).
Синтаксис .then() (Старый стиль, но его нужно уметь читать):
// Функция fetch возвращает Promise
fetch('https://api.example.com/user/1')
.then(response => {
// Этот код выполнится, когда сервер ответит
return response.json(); // .json() тоже возвращает Promise!
})
.then(data => {
// Этот код выполнится, когда данные распарсятся
console.log("User data:", data);
})
.catch(error => {
// Ловим ошибки любого из этапов выше
console.error("Something went wrong:", error);
})
.finally(() => {
// Выполняется всегда (например, скрыть спиннер загрузки)
console.log("Request finished");
});Это "синтаксический сахар" над промисами. Он позволяет писать асинхронный код так, будто он синхронный (линейный). Это делает код гораздо чище и понятнее, особенно для бэкенд-разработчиков.
Ключевые правила:
- Ключевое слово
asyncперед функцией делает так, что она всегда возвращает Promise. - Ключевое слово
awaitможно использовать только внутриasyncфункций. await"ставит на паузу" выполнение функции (но не блокирует браузер!), пока Промис не перейдет в состояние Resolved.
Пример (тот же запрос, но современно):
// Создаем асинхронную функцию
const getUserData = async (id) => {
try {
// JS "ждет" здесь, пока fetch не вернет ответ.
// В это время поток свободен для отрисовки интерфейса.
const response = await fetch(`https://api.example.com/user/${id}`);
// Проверка на HTTP ошибки (fetch не кидает throw на 404/500)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// Ждем парсинг JSON
const data = await response.json();
return data; // Это значение обернется в Promise автоматически
} catch (error) {
// Здесь мы ловим и ошибки сети, и наши ручные throw
console.error("Ошибка загрузки:", error);
return null;
}
};
// Вызов:
// getUserData(1).then(data => console.log(data));Почему это важно знать? Потому что рендеринг React (обновление DOM) и выполнение вашего JS кода делят один поток.
- Call Stack (Стек вызовов): Здесь выполняется обычный JS код.
- Web APIs: Когда вы делаете
fetchилиsetTimeout, задача уходит из стека браузеру ("Браузер, пни меня, когда таймер истечет"). - Task Queue (Очередь задач): Когда таймер истек или данные пришли, колбэк (код после
awaitили внутри.then) попадает в очередь. - Event Loop: Бесконечный цикл, который смотрит: "Если Стек пуст, возьми задачу из Очереди".
Важный вывод для React: Никогда не выполняйте тяжелые синхронные операции (например, сложные математические циклы на 10^9 итераций) прямо в теле компонента или useEffect. Это "подвесит" Event Loop, и пользователь не сможет даже нажать на кнопку.
Вы забегаем чуть вперед, но важно увидеть контекст. В React мы не можем сделать сам компонент асинхронным (пока что, в клиентских компонентах).
Неправильно:
// ❌ Компонент не может быть async
const UserProfile = async () => {
const data = await fetch(...); // Так нельзя!
return <div>{data.name}</div>;
}Правильно (Паттерн "Fetch on Render" с useEffect):
Мы создаем асинхронную функцию внутри эффекта и вызываем её.
import { useState, useEffect } from 'react';
const UserProfile = () => {
const [user, setUser] = useState(null);
useEffect(() => {
// Объявляем асинхронную функцию внутри
const fetchData = async () => {
try {
const response = await fetch('/api/user');
const result = await response.json();
setUser(result); // Обновляем состояние -> вызывает ререндер
} catch (e) {
console.error(e);
}
};
// Вызываем её
fetchData();
}, []); // [] означает "выполнить один раз при монтировании"
if (!user) return <div>Загрузка...</div>;
return <div>Привет, {user.name}</div>;
};В JavaScript (как и в Java) примитивы (string, number, boolean) передаются по значению, а объекты и массивы — по ссылке.
React должен знать, когда перерисовать компонент. Чтобы это понять, он сравнивает старое состояние (State) и новое.
- Если бы React делал глубокое сравнение (Deep Compare) каждого поля вложенного объекта, это убило бы производительность при каждом чихе.
- Поэтому React делает поверхностное сравнение (Shallow Compare) ссылок. Он просто проверяет:
oldObject === newObject.
Если вы измените поле внутри объекта, но сохраните ссылку на сам объект — React подумает, что ничего не изменилось, и не перерисует интерфейс.
const user = { name: "Dmitry", role: "Admin" };
// Мы изменили свойство, но ссылка 'user' осталась той же.
// В памяти это та же область.
user.name = "Alex";
// React сравнит: (user === user) -> true.
// Ререндера НЕ будет. Интерфейс покажет старое имя.Мы используем Spread-оператор (...), чтобы создать новую копию объекта с измененным полем.
const user = { name: "Dmitry", role: "Admin" };
// 1. Создаем новый объектный литерал {}
// 2. Копируем все поля из user (...user)
// 3. Перезаписываем name
// В памяти создается НОВАЯ область.
const updatedUser = { ...user, name: "Alex" };
// React сравнит: (user === updatedUser) -> false.
// Ссылки разные. Ререндер БУДЕТ.С массивами сложнее, так как многие стандартные методы JS мутируют исходный массив. В React их использовать нельзя (или только над копиями).
Эти методы меняют массив "на месте". Избегайте их при работе со стейтом напрямую.
push(),pop()shift(),unshift()splice()sort(),reverse()
Эти методы возвращают новый массив, оставляя старый нетронутым.
map()— трансформация.filter()— удаление/фильтрация.reduce()— агрегация.concat()или Spread[...]— добавление.slice()— копирование части.
1. Добавление элемента (вместо push):
const numbers = [1, 2, 3];
// ❌ numbers.push(4) — мутация!
// ✅ Создаем новый массив, разворачиваем старый + новый элемент
const newNumbers = [...numbers, 4]; 2. Удаление элемента (вместо splice):
const users = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" }, // Хотим удалить Bob
{ id: 3, name: "Charlie" }
];
// ✅ Используем filter. Он вернет новый массив без элемента.
// Читается как: "Оставь только тех, у кого id НЕ равен 2"
const usersWithoutBob = users.filter(user => user.id !== 2);3. Обновление элемента в массиве (самое сложное):
Допустим, нам нужно изменить имя пользователя с id: 2.
const users = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" }
];
// ✅ Используем map. Он проходит по каждому элементу и создает новый массив.
const updatedUsers = users.map(user => {
if (user.id === 2) {
// Если это тот, кого ищем — возвращаем КОПИЮ с новыми данными
return { ...user, name: "Robert" };
}
// Иначе возвращаем объект как есть (ссылка на него не меняется)
return user;
});Оператор ... делает только поверхностную копию. Если у вас глубокая вложенность, spread копирует только ссылки на вложенные объекты.
const state = {
settings: {
theme: "dark",
notifications: true
}
};
// ❌ Ошибка новичка:
// const newState = { ...state };
// newState.settings.theme = "light"; // МУТАЦИЯ! newState.settings ссылается на тот же объект в памяти.
// ✅ Правильный подход для вложенности (Deep Update):
const newState = {
...state, // Копируем верхний уровень
settings: { // Явно создаем новый объект для settings
...state.settings, // Копируем поля settings
theme: "light" // Меняем нужное
}
};Примечание: Для глубоких обновлений в больших проектах часто используют библиотеку Immer или structuredClone (но в React-сообществе чаще всего достаточно spread или Immer).
Аналог в Java: JVM (Java Virtual Machine).
Даже если вы пишете Frontend, вам нужен Node.js. Почему?
- Build Tools: Компиляторы (Babel, TypeScript), сборщики (Vite, Webpack), линтеры (ESLint) написаны на JavaScript и запускаются в среде Node.js.
- Dev Server: Локальный сервер для разработки поднимается на Node.js.
Вам не нужно учить API Node.js (работу с файловой системой или потоками), если вы не пишете Backend, но он должен быть установлен.
Аналог в Java: Maven или Gradle.
Аналог package.json: pom.xml или build.gradle.
Аналог node_modules: Папка .m2 (но с важным отличием: в JS зависимости скачиваются в каждую папку проекта, а не в глобальный кэш).
Мы используем менеджеры пакетов для управления зависимостями (React, TypeScript, CSS-библиотеки).
- npm (Node Package Manager): Идет в комплекте с Node.js. Стандарт, надежный, но иногда медленный.
- yarn: Был создан Facebook для ускорения npm. Сейчас разница невелика.
- pnpm (Performant npm): Рекомендуемый выбор в 2025.
- Почему: Он использует жесткие ссылки (hard links) и глобальное хранилище. Если у вас 10 проектов на React, pnpm сохранит React на диске один раз, а в проекты положит ссылки. Это экономит гигабайты места (в отличие от npm, который скачает React 10 раз).
Раньше стандартом был Create React App (CRA), который под капотом использовал Webpack. Сейчас CRA официально deprecated (устарел), а документация React рекомендует использовать фреймворки (Next.js) или Vite для чистых SPA.
Webpack — это "Bundler". Чтобы запустить локальный сервер, он должен:
- Прочитать ВСЕ файлы проекта.
- Скомпилировать их.
- Собрать в один большой
bundle.js. - И только потом отдать браузеру. В больших проектах запуск занимал минуты. Любое изменение кода требовало пересборки части бандла.
Vite использует современные возможности браузеров — Native ES Modules (ESM).
- Dev Server: Vite не собирает бандл при старте. Он сразу запускает сервер.
- On Demand: Когда браузер запрашивает страницу, он видит
import App from './App.js'. Браузер сам идет за этим файлом. Vite на лету компилирует только этот один файл и отдает его браузеру. - HMR (Hot Module Replacement): Замена модуля происходит мгновенно, без перезагрузки страницы.
Давайте создадим заготовку вашего будущего приложения. Я предполагаю, что Node.js у вас уже установлен.
Шаг 1. Создание проекта через Vite Откройте терминал.
# npm create vite@latest <имя-папки> -- --template react
# Но мы будем использовать pnpm, если хотите (или npm, если pnpm нет)
npm create vite@latest my-react-app -- --template react
# Вас могут спросить про фреймворк: выбираем React.
# Вас спросят про язык: выбираем JavaScript (пока без TypeScript, перейдем к нему в Главе 13, чтобы не усложнять старт).
Шаг 2. Запуск
cd my-react-app
npm install # Скачивание зависимостей (создание папки node_modules)
npm run dev # Запуск локального сервера
Вы увидите что-то вроде:
➜ Local: http://localhost:5173/
Шаг 3. Разбор структуры (Анатомия)
Откройте папку в IDE (VS Code или WebStorm/IntelliJ).
-
node_modules/— Библиотеки. В git не коммитим (добавлено в .gitignore). -
public/— Статические файлы (favicon, robots.txt), которые копируются "как есть". -
src/— Ваш исходный код. -
main.jsx— Точка входа (какpublic static void main). Здесь React "цепляется" к HTML. -
App.jsx— Корневой компонент. -
App.css/index.css— Стили. -
index.html— Единственный HTML файл. В нем есть<div id="root"></div>. Именно сюда React будет рендерить всё приложение. -
package.json— Список зависимостей и скрипты запуска. -
vite.config.js— Конфигурация сборщика.
Откройте src/main.jsx. Вы увидите:
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import './index.css'
// React находит div с id="root" в index.html и создает там "корень"
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)- React.StrictMode: Специальная обертка для разработки. Она специально вызывает рендер компонентов дважды, чтобы выявить небезопасные побочные эффекты (мы поговорим об этом в главе про useEffect). Не пугайтесь, если увидите дублирующиеся логи в консоли — это нормально в Dev режиме.
Императивный подход (Как в jQuery или чистом JS) Вы говорите браузеру КАК сделать изменения. Вы вручную находите элементы и меняете их свойства шаг за шагом.
// ❌ Imperative (Java Swing style / Vanilla JS)
// Сценарий: Пользователь залогинился
const header = document.getElementById('header');
const loginBtn = document.getElementById('login-btn');
const welcomeMsg = document.createElement('p');
loginBtn.remove(); // 1. Удали кнопку
welcomeMsg.innerText = 'Hello, User'; // 2. Создай текст
header.appendChild(welcomeMsg); // 3. Вставь текст
header.style.backgroundColor = 'blue'; // 4. Покрась фонПроблема: С ростом приложения вы теряетесь в том, в каком состоянии находится интерфейс сейчас. Нужно держать в голове всю цепочку мутаций.
Декларативный подход (React) Вы описываете, ЧТО вы хотите видеть для конкретного состояния данных. React сам решает, как достичь этого результата (удалить кнопку, добавить текст и т.д.).
// ✅ Declarative (React)
// Состояние (State) — единственный источник правды
const Header = ({ isLoggedIn, userName }) => {
if (isLoggedIn) {
// Мы просто возвращаем финальный вид
return (
<header className="bg-blue">
<p>Hello, {userName}</p>
</header>
);
}
// Или этот вид, если не залогинен
return (
<header>
<button>Login</button>
</header>
);
};Преимущество: Предсказуемость. Если isLoggedIn == true, интерфейс всегда будет выглядеть одинаково, независимо от того, что происходило до этого.
Почему React не обновляет реальный DOM сразу? Потому что DOM (дерево HTML-элементов браузера) — это очень медленная структура. Чтение и запись в реальный DOM — "дорогие" операции.
Virtual DOM — это легковесная копия DOM, хранящаяся в памяти как обычный JavaScript-объект.
Алгоритм обновления (Reconciliation):
- Render: Произошло изменение данных (State изменился). React вызывает ваши функции-компоненты и строит новое дерево Virtual DOM.
- Diffing: React сравнивает (diff) новое дерево Virtual DOM со старым (которое было на предыдущем кадре).
- Commit: Он вычисляет минимальный набор изменений (патч) и применяет их к реальному DOM.
Пример:
Если в списке из 1000 элементов поменялся текст только в одном, React не будет перерисовывать весь список <ul>. Он точечно изменит innerText одного <li>.
JSX (JavaScript XML) выглядит как HTML, но это синтаксический сахар.
Под капотом это компилируется в вызовы JS функций:
// Вы пишете:
const element = <h1 className="title">Hello</h1>;
// Vite/Babel превращает это в:
const element = React.createElement('h1', { className: 'title' }, 'Hello');Ключевые правила JSX:
- Один родитель: Компонент должен возвращать один корневой элемент.
// ❌ Ошибка
return (
<h1>Header</h1>
<p>Text</p>
);
// ✅ Правильно (Обертка div или Fragment)
return (
<div>
<h1>Header</h1>
<p>Text</p>
</div>
);
// ✅ Fragment (не создает лишнего узла в DOM)
return (
<>
<h1>Header</h1>
<p>Text</p>
</>
);- JavaScript внутри
{}: Чтобы вывести переменную или результат выражения, используйте фигурные скобки.
const name = "Dmitry";
const isAdmin = true;
return (
<div className="card">
<h1>{name}</h1>
<p>Status: {isAdmin ? "Admin" : "User"}</p>
<p>Score: {10 + 20}</p>
{/* Комментарии в JSX тоже пишутся в скобках */}
</div>
);- Атрибуты camelCase: Поскольку это JS, мы не можем использовать зарезервированные слова:
class->classNamefor->htmlFor(в лейблах)onclick->onClicktabindex->tabIndex
- Закрывайте теги:
В HTML
<input>валиден. В JSX все теги должны быть закрыты:<input />,<br />,<img />.
В современном React компоненты — это просто функции.
// src/components/WelcomeCard.jsx
// 1. Импорты (если нужны)
import "./WelcomeCard.css";
// 2. Объявление функции (Компонент всегда с Большой Буквы!)
// Принимает объект props (аргументы)
const WelcomeCard = (props) => {
// 3. Логика (до return)
const isMorning = new Date().getHours() < 12;
const greeting = isMorning ? "Good Morning" : "Hello";
// 4. Возврат JSX (Шаблон)
return (
<div className="card-container">
<h2>{greeting}, {props.name}!</h2>
<button onClick={() => alert("Clicked!")}>
Click me
</button>
</div>
);
};
// 5. Экспорт
export default WelcomeCard;Использование компонента в App.js:
import WelcomeCard from './components/WelcomeCard';
function App() {
return (
<div>
{/* Мы используем его как кастомный HTML тег */}
<WelcomeCard name="Dmitry" />
<WelcomeCard name="Alex" />
</div>
);
}Props (сокращение от properties) — это входные данные компонента. Для Java-разработчика лучшая аналогия: Props — это аргументы конструктора или метода.
Главное правило: Поток данных в React однонаправленный (Unidirectional Data Flow). Данные текут строго сверху вниз (от родителя к ребенку). Ребенок не может менять свои пропсы (они read-only), он может только попросить родителя изменить их (через колбэки, об этом в главе про Events).
// Родительский компонент
function UserProfile() {
const userData = { name: "Dmitry", role: "Java Expert" };
return (
<div className="profile">
{/* Передаем данные вниз через атрибуты */}
<Avatar url="/img/avatar.png" size={64} />
<InfoBox title="User Info" data={userData} />
</div>
);
}
// Дочерний компонент
// Сразу используем деструктуризацию (см. Глава 1.1)
function InfoBox({ title, data }) {
// data = { name: "Dmitry", ... }
return (
<div className="box">
<h3>{title}</h3>
<p>Name: {data.name}</p>
<p>Role: {data.role}</p>
</div>
);
}Props children:
Существует специальный проп children. Это всё, что находится внутри открывающего и закрывающего тега компонента. Это аналог слотов (slots) в других фреймворках.
function Card({ children }) {
return <div className="card-border">{children}</div>;
}
// Использование:
<Card>
<h1>Заголовок</h1> {/* Это попадет в children */}
<p>Контент</p> {/* Это тоже */}
</Card>В React нет циклов for внутри JSX (так как JSX — это выражение). Для отрисовки списков мы используем метод массива .map(), который преобразует массив данных в массив JSX-элементов.
const todoList = [
{ id: 1, text: "Выучить React" },
{ id: 2, text: "Написать книгу по Go" },
{ id: 3, text: "Настроить MinQLX" }
];
function TodoList() {
return (
<ul>
{todoList.map((item) => (
// Key обязателен на верхнем элементе внутри map!
<li key={item.id}>
{item.text}
</li>
))}
</ul>
);
}Зачем нужен key?
Это критически важный момент для механизма Reconciliation (сверки Virtual DOM).
Представьте, что вы вставили новый элемент в начало списка из 1000 пунктов.
- Без ключей: React сравнит первый элемент DOM с первым элементом новых данных. Они разные. Он перерисует первый, потом второй, и так весь список. Это O(N).
- С ключами (id): React увидит: "Ага, элемент с key=100 просто сдвинулся вниз, а key=101 новый". Он вставит один элемент и передвинет остальные.
Антипаттерн: Использование индекса массива map((item, index) => ... key={index}).
- Почему плохо: Если вы удалите элемент из середины или отсортируете список, индексы изменятся. React перепутает состояния компонентов (например, введенный текст в инпутах может "перескочить" на другую строку).
- Правило: Используйте уникальные ID из базы данных. Индекс можно брать только если список статичен и никогда не меняется.
В JSX нельзя писать if-else (это инструкции), но можно использовать тернарные операторы и логическое И (&&).
1. Тернарный оператор (If-Else): Идеально для переключения между двумя состояниями.
function LoginButton({ isLoggedIn }) {
return (
<button>
{isLoggedIn ? "Выйти" : "Войти"}
</button>
);
}2. Логическое И (Short-circuit &&):
Используется, когда нужно показать элемент только если условие истинно (аналог if без else).
function Inbox({ unreadMessages }) {
return (
<div>
<h1>Входящие</h1>
{/* Если unreadMessages.length > 0, то рендерим span. Иначе — null (ничего) */}
{unreadMessages.length > 0 && (
<span className="badge">{unreadMessages.length}</span>
)}
</div>
);
}Осторожно: В JS 0 — это falsy, но React рендерит число 0.
Если написать {messages.length && <Badge />} и сообщений 0, в интерфейсе нарисуется цифра 0.
Fix: Всегда приводите к булеву (!!length) или пишите явное сравнение (length > 0).
Синтаксис похож на HTML, но есть отличия:
- События именуются в camelCase (
onClick,onSubmit,onChange). - Передаем функцию, а не строку.
function Button() {
// Event Handler
const handleClick = (e) => {
// e — это SyntheticEvent (обертка React над нативным событием браузера)
e.preventDefault(); // Предотвратить дефолтное поведение (например, отправку формы)
console.log("Button clicked!");
};
return (
// ✅ Правильно: Передаем ссылку на функцию
<button onClick={handleClick}>Нажми меня</button>
// ❌ Ошибка новичка: Вызов функции сразу при рендере
// <button onClick={handleClick()}>Нажми меня</button>
);
}Передача параметров: Если нужно передать аргумент в обработчик, оборачиваем его в стрелочную функцию.
<button onClick={() => handleDelete(id)}>Удалить</button>React ожидает, что ваши компоненты ведут себя как Чистые функции (во время рендеринга).
- Чистая функция: При одних и тех же входных данных (Props) всегда возвращает один и тот же результат (JSX) и не имеет побочных эффектов (Side Effects).
Запрещено делать в теле компонента (во время рендера):
- Менять переменные, объявленные снаружи компонента.
- Делать сетевые запросы (
fetch). - Менять DOM напрямую.
Где делать побочные эффекты?
В обработчиках событий (onClick) или в useEffect (об этом в Главе 7).
// ❌ Грязный компонент (Ошибка)
let guestCount = 0; // Внешняя переменная
function Cup() {
guestCount++; // Изменение внешней переменной при рендере!
return <h2>Guest #{guestCount}</h2>;
}
// При каждом ререндере (даже если данные не менялись) счетчик будет расти непредсказуемо.Это то, с чего начинают все. Это работает точно так же, как в обычной верстке, но с одним отличием: вместо атрибута class мы используем className (так как class — зарезервированное слово в JS).
Как это выглядит:
- Создаем файл
Button.css. - Импортируем его в компонент.
/* Button.css */
.btn {
padding: 10px 20px;
border-radius: 5px;
background-color: blue;
color: white;
}// Button.jsx
import './Button.css'; // Импорт делает стили глобальными!
const Button = () => {
return <button className="btn">Click me</button>;
};Проблема:
Все стили в React при таком подходе — глобальные.
Если вы создадите компонент Header с классом .container и компонент Footer с классом .container, их стили перепишут друг друга (Cascading). В больших проектах это превращается в ад ("Кто перекрасил мою кнопку?!").
Чтобы решить проблему глобальных конфликтов, придумали CSS Modules. Это стандарт де-факто для тех, кто любит писать классический CSS, но хочет безопасности.
Правило: Файл должен называться [name].module.css.
Как это работает:
Сборщик (Vite/Webpack) берет ваши классы и автоматически добавляет к ним уникальный хэш.
.btn превращается в .Button_btn__a1b2c.
/* Button.module.css */
.btn {
background-color: green;
/* ... */
}
.error {
background-color: red;
}// Button.jsx
// Обратите внимание: мы импортируем ОБЪЕКТ styles
import styles from './Button.module.css';
const Button = ({ isError }) => {
// styles.btn — это строка "Button_btn__a1b2c"
return (
<button className={isError ? styles.error : styles.btn}>
Click me
</button>
);
};Плюсы: Полная изоляция. Вы можете использовать класс .container в каждом файле, и они никогда не пересекутся.
Сейчас это самый популярный выбор для новых проектов (startups, enterprise). Идея: вместо написания CSS-файлов мы используем готовые утилитарные классы прямо в HTML.
Философия: "Вам не нужно уходить из HTML-файла (JSX), чтобы поправить отступы".
Сравнение:
- Classic CSS:
.card {
background-color: white;
border-radius: 0.25rem;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
padding: 1rem;
}- Tailwind CSS:
<div className="bg-white rounded shadow-md p-4">...</div>Почему это круто (для Java-разработчика): Это похоже на типизированный API для стилей.
- Скорость: Вы не придумываете названия классов (
wrapper-inner-left-bottom). - Консистентность: Вы не можете написать
padding: 13px. Вы обязаны выбрать из дизайн-системы:p-3(12px) илиp-4(16px). Интерфейс выглядит ровным. - Dead Code Elimination: При сборке в итоговый CSS попадают только те классы, которые вы использовали. Файл стилей остается крошечным.
Пример компонента на Tailwind:
const UserCard = ({ name, role }) => {
return (
// flex, items-center (align-items), space-x-4 (gap)
<div className="flex items-center space-x-4 p-6 bg-white rounded-xl shadow-lg hover:shadow-xl transition-shadow">
<div className="text-xl font-medium text-black">
{name}
</div>
<p className="text-gray-500">
{role}
</p>
</div>
);
};В React часто нужно менять классы динамически (например, кнопка активна/неактивна). Делать это через шаблонные строки неудобно и некрасиво:
// ❌ Грязно
<div className={`btn ${isActive ? 'active' : ''} ${isDisabled ? 'disabled' : ''}`}>Для этого используют крошечные библиотеки clsx или classnames (они почти идентичны).
Установка: npm install clsx
Использование:
import clsx from 'clsx';
const Button = ({ variant = 'primary', isDisabled, isLoading }) => {
return (
<button
className={clsx(
// Базовые классы (всегда есть)
'px-4 py-2 rounded font-bold text-white',
// Условия
{
'bg-blue-500 hover:bg-blue-700': variant === 'primary',
'bg-red-500 hover:bg-red-700': variant === 'danger',
'opacity-50 cursor-not-allowed': isDisabled,
'animate-pulse': isLoading
}
)}
>
Click me
</button>
);
};Это делает код JSX чистым и читаемым.
Для работы нужно установить библиотеку:
npm install styled-components
Концепция: Мы создаем компоненты, которые уже имеют стили. Нам не нужно придумывать имена классов (className).
Используется синтаксис Tagged Template Literals (обратные кавычки ...), который мы разбирали в главе 1.
import styled from 'styled-components';
// 1. Мы создаем переменную Title.
// 2. styled.h1 говорит: "Создай React-компонент, который рендерит тег <h1>".
// 3. Внутри кавычек пишем обычный CSS.
const Title = styled.h1`
font-size: 1.5em;
text-align: center;
color: palevioletred;
`;
// Создаем компонент-обертку (div)
const Wrapper = styled.section`
padding: 4em;
background: papayawhip;
`;
// Использование в React (выглядит как обычные компоненты)
const App = () => {
return (
<Wrapper>
<Title>Hello World!</Title>
</Wrapper>
);
};Что происходит в DOM?
Библиотека сгенерирует уникальный хеш-класс (например, .sc-a1b2c) и вставит тег <style> в <head> документа. Стили полностью изолированы.
Это "киллер-фича" CSS-in-JS. Поскольку мы находимся внутри JavaScript файла, мы можем внедрять функции прямо в CSS. Стили могут меняться в зависимости от пропсов.
// Компонент Button
const Button = styled.button`
background: ${props => props.$primary ? "blue" : "white"};
color: ${props => props.$primary ? "white" : "blue"};
font-size: 1em;
margin: 1em;
padding: 0.25em 1em;
border: 2px solid blue;
border-radius: 3px;
cursor: pointer;
`;
// Использование
const App = () => {
return (
<div>
{/* Обычная кнопка (белая) */}
<Button>Normal</Button>
{/* Первичная кнопка (синяя) */}
<Button $primary>Primary</Button>
</div>
);
};Примечание: В новых версиях styled-components рекомендуется использовать префикс $ для пропсов, которые нужны только для стилей (transient props), чтобы они не попадали в HTML-атрибуты в DOM.
Часто нужно взять уже готовую кнопку и чуть-чуть её изменить (например, кнопку "Удалить").
// Берем готовую Button и добавляем стили поверх
const DeleteButton = styled(Button)`
color: red;
border-color: red;
background: transparent;
`;Styled Components предоставляет мощный механизм для глобальных тем (светлая/темная тема, фирменные цвета). Это делается через ThemeProvider.
// 1. Создаем объект темы (как константы в Java)
const theme = {
colors: {
main: "mediumseagreen",
secondary: "palevioletred",
bg: "#f0f0f0"
},
breakpoints: {
mobile: "768px"
}
};
// 2. Оборачиваем все приложение в ThemeProvider
import { ThemeProvider } from 'styled-components';
const App = () => (
<ThemeProvider theme={theme}>
<MyComponent />
</ThemeProvider>
);
// 3. Теперь в ЛЮБОМ styled-компоненте мы имеем доступ к theme через props
const Box = styled.div`
background-color: ${props => props.theme.colors.bg};
@media (max-width: ${props => props.theme.breakpoints.mobile}) {
flex-direction: column;
}
`;Для уровня "Архитектор" важно понимать цену выбора.
Плюсы Styled Components:
- Критический CSS: Библиотека автоматически отслеживает, какие компоненты на экране, и загружает только их стили.
- Нет конфликтов имен: Вообще. Никогда.
- Удобный DX (Developer Experience): Удаляя JS-файл компонента, вы удаляете и его стили. Нет "мертвого кода" в CSS файлах.
- Unit-тестирование стилей: Легко тестировать логику отображения (
toHaveStyleRule).
Минусы (почему сейчас тренд смещается к Tailwind или CSS Modules):
- Runtime Overhead: Библиотека весит (около 12kb gzip), и, что важнее, парсинг стилей и генерация классов происходит в браузере пользователя во время работы JS. Это может тормозить на слабых устройствах.
- Размер бандла: Стили включены в JS, что увеличивает время парсинга скриптов.
Современные альтернативы (Zero-runtime CSS-in-JS):
Библиотеки вроде Linaria или Vanilla Extract позволяют писать так же, как в styled-components, но во время сборки (Build time) они извлекают всё это в обычный .css файл. Это идеальный баланс, но сложнее в настройке.
Это главное концептуальное разделение.
- Classic UI Kits (Комбайны):
- Примеры: Material UI (MUI), Ant Design, Bootstrap React.
- Суть: Вы получаете компонент "Кнопка", который уже имеет цвета, отступы, анимации и логику.
- Аналогия с Java: Это как Spring Boot. Всё настроено за вас, но если нужно поменять что-то глубоко внутри — придется попотеть, переопределяя дефолты.
- Headless UI (Только логика):
- Примеры: Radix UI, Headless UI, React Aria.
- Суть: Библиотека дает вам функциональность (как открывается модальное окно, как работает фокус, доступность/a11y), но 0 стилей. Вы сами решаете, как это выглядит (обычно через Tailwind).
- Аналогия с Java: Это как подключить библиотеку для алгоритмов. Логика есть, но интерфейс вы пишете сами.
Эти библиотеки идеальны для B2B, CRM, Admin Panels, где скорость разработки важнее уникального дизайна.
MUI (Material UI) — реализация Google Material Design. Самая популярная библиотека в мире React.
// npm install @mui/material @emotion/react @emotion/styled
import Button from '@mui/material/Button';
import TextField from '@mui/material/TextField';
function LoginForm() {
return (
<form style={{ display: 'flex', gap: '10px', flexDirection: 'column' }}>
<TextField label="Username" variant="outlined" />
<TextField label="Password" type="password" />
{/* Кнопка уже с ripple-эффектом, тенями и hover-состояниями */}
<Button variant="contained" color="primary">
Login
</Button>
</form>
);
}Плюсы:
- Скорость: Можно собрать админку за вечер.
- Документация: Огромная база знаний.
- Data Grid: Мощнейшие таблицы из коробки.
Минусы:
- Bundle Size: Тянет за собой много кода.
- "Google Look": Все сайты на MUI выглядят одинаково. Кастомизация требует борьбы с их движком стилизации.
Это подход, который сейчас доминирует в новых продуктах.
Radix UI — это набор низкоуровневых примитивов (Primitives).
Например, Dialog в Radix берет на себя управление фокусом (чтобы фокус не ушел под модалку), закрытие по ESC, блокировку скролла. Но выглядит он никак (прозрачный).
Shadcn/ui — это НЕ библиотека в привычном понимании (не npm package). Это коллекция переиспользуемых компонентов, построенных на Radix UI и Tailwind CSS.
Как это работает:
Вы не делаете npm install shadcn-ui. Вы запускаете команду, которая копирует исходный код компонента в вашу папку src/components/ui.
npx shadcn-ui@latest add button
# В папке src/components/ui/button.jsx появляется файл с кодом кнопки.
Пример (что вы получаете в коде):
// src/components/ui/button.jsx (код полностью ваш!)
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva } from "class-variance-authority" // Утилита для вариантов стилей
import { cn } from "@/lib/utils" // Обертка над clsx + tailwind-merge
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors ...",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
const Button = React.forwardRef(({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
})
Button.displayName = "Button"
export { Button, buttonVariants }Почему это круто для Архитектора:
- Full Control: Вы владеете кодом. Хотите изменить отступы во всех кнопках? Правите файл у себя в проекте. Нет никакой закрытой "черной коробки" в
node_modules. - Accessibility (a11y): Radix гарантирует доступность.
- Design System: Это идеальный старт для создания своей дизайн-системы.
Забудьте про подключение FontAwesome через CDN. В React иконки — это компоненты (SVG).
Lucide React — текущий стандарт. Это красивый, легкий и консистентный набор иконок (форк Feather Icons).
// npm install lucide-react
import { Camera, Home, User } from 'lucide-react';
function Menu() {
return (
<nav>
{/* Иконка - это просто компонент. Можно красить через color или class */}
<Home size={24} color="red" />
<Camera className="text-blue-500 w-6 h-6" />
</nav>
);
}Важно: Благодаря Tree Shaking (тряска дерева) в итоговый бандл попадут только те 2 иконки, которые вы импортировали, а не весь пакет из 1000 иконок.
CSS animations хороши для простых ховеров. Для сложной оркестрации (появление списка, переходы между страницами) используют Framer Motion.
Это библиотека декларативных анимаций.
// npm install framer-motion
import { motion } from 'framer-motion';
const Box = () => (
<motion.div
// Начальное состояние
initial={{ opacity: 0, scale: 0.5 }}
// Конечное состояние
animate={{ opacity: 1, scale: 1 }}
// Настройки перехода
transition={{ duration: 0.5 }}
className="w-20 h-20 bg-blue-500"
/>
);С Framer Motion можно легко делать вещи, которые на чистом CSS/JS занимают сотни строк: Drag & Drop, Layout Animations (когда элементы плавно меняются местами при удалении одного из списка).
В функциональном программировании функции должны быть чистыми. Но интерфейс — это состояние (открыто ли меню, что введено в инпут). useState позволяет компоненту "запомнить" информацию между рендерами.
Синтаксис:
const [state, setState] = useState(initialValue);Ключевые моменты для Java-разработчика:
- Это не просто переменная. Изменение переменной
letне обновит экран. ВызовsetState— это триггер: "React, данные изменились, вызови эту функцию-компонент еще раз и перерисуй DOM!". - Асинхронность (Batching). Обновление стейта не происходит мгновенно (в той же строке кода). React планирует обновление.
Пример 1: Базовое использование
import { useState } from 'react';
const Counter = () => {
// 1. Инициализация. Выполняется только при ПЕРВОМ рендере.
// count — текущее значение (0).
// setCount — функция-сеттер.
const [count, setCount] = useState(0);
const handleClick = () => {
// 2. Обновление.
setCount(count + 1);
// ⚠️ ВНИМАНИЕ: Здесь count все еще равен 0!
// Обновленное значение придет только в СЛЕДУЮЩЕМ запуске функции Counter.
console.log(count); // Выведет старое значение
};
return (
<button onClick={handleClick}>
Count: {count}
</button>
);
};Пример 2: Functional Update (Важно!) Если новое состояние зависит от старого, всегда используйте колбэк-версию. Это защищает от багов при частых обновлениях (race conditions).
const handleDoubleIncrement = () => {
// ❌ Плохо: Если нажать быстро, React может "склеить" обновления
// setCount(count + 1);
// setCount(count + 1); // Результат будет +1, а не +2
// ✅ Хорошо: Functional Update
// prev — это гарантированно актуальное значение на момент выполнения
setCount(prev => prev + 1);
setCount(prev => prev + 1); // Результат будет +2
};Функциональный компонент должен быть чистым (только возвращать JSX). Но нам нужны побочные эффекты (Side Effects): запросы к API, подписки на WebSocket, изменение document.title.
useEffect — это место для грязной работы.
Ментальная модель: Не думайте "когда компонент создался". Думайте "синхронизировать эффект с состоянием".
Синтаксис:
useEffect(() => {
// Тело эффекта
return () => {
// Функция очистки (Cleanup)
};
}, [dependencies]); // Массив зависимостейСценарий A: "ComponentDidMount" (Один раз при старте)
Пустой массив зависимостей [] говорит React: "Этот эффект не зависит ни от каких пропсов или стейта, запусти его один раз после первого рендера".
useEffect(() => {
console.log("Компонент смонтирован (Mounted)");
// Идеальное место для API вызова
fetchData();
}, []); Сценарий B: "ComponentDidUpdate" (Реакция на изменения) Если указать переменную в массиве, эффект перезапустится, когда эта переменная изменится.
const [userId, setUserId] = useState(1);
useEffect(() => {
// Запускается при старте И каждый раз, когда меняется userId
console.log(`Загружаю данные для user: ${userId}`);
fetch(`/api/users/${userId}`);
}, [userId]); // <--- ЗависимостьСценарий C: "ComponentWillUnmount" (Очистка / Cleanup)
Если эффект создает подписку (таймер, слушатель событий), её нужно отменить, иначе будет утечка памяти. Функция, которую возвращает useEffect, запускается перед следующим запуском эффекта или когда компонент удаляется.
useEffect(() => {
const handleResize = () => console.log(window.innerWidth);
// 1. Подписываемся
window.addEventListener('resize', handleResize);
// 2. Возвращаем функцию очистки
return () => {
// 3. Отписываемся (обязательно!)
window.removeEventListener('resize', handleResize);
};
}, []);useRef возвращает мутабельный объект { current: ... }.
Он похож на useState, но с одним отличием: изменение ref.current НЕ вызывает ререндер.
Применение 1: Доступ к DOM (аналог document.getElementById) В React мы редко лезем в DOM напрямую, но иногда нужно (фокус, скролл, замер размера).
import { useRef, useEffect } from 'react';
const InputFocus = () => {
const inputRef = useRef(null); // Изначально null
useEffect(() => {
// После рендера inputRef.current будет указывать на реальный DOM-элемент <input>
inputRef.current.focus();
}, []);
// Привязываем реф к элементу через атрибут ref
return <input ref={inputRef} />;
};Применение 2: Хранение значений "мимо" рендера Например, нам нужно хранить ID таймера, чтобы очистить его. Нам не нужно перерисовывать экран, когда ID меняется.
const timerId = useRef(null);
const startTimer = () => {
// Мы меняем current, ререндера нет, но значение сохраняется между вызовами функции компонента
timerId.current = setInterval(() => console.log('Tick'), 1000);
};
const stopTimer = () => {
clearInterval(timerId.current);
};React полагается на порядок вызова хуков, чтобы знать, какой стейт к какому useState относится. Поэтому есть два жестких правила (за ними следит линтер):
- Только на верхнем уровне: Нельзя вызывать хуки внутри циклов
for, условийifили вложенных функций.
// ❌ ОШИБКА
if (isAdmin) {
const [data, setData] = useState(); // Порядок хуков собьется!
}
// ✅ ПРАВИЛЬНО
const [data, setData] = useState();
if (isAdmin) { ... }- Только в React-функциях: Хуки можно вызывать только внутри функциональных компонентов или внутри других (кастомных) хуков. Нельзя вызывать их в обычных JS-классах или вспомогательных функциях.
Раньше, чтобы узнать, доскроллил ли пользователь до конца страницы или до конкретного блока, мы вешали событие onScroll на окно. Это вызывало функцию сотни раз в секунду и тормозило браузер.
Современный стандарт — Intersection Observer API. Он работает асинхронно и сообщает нам: "Эй, этот div пересек границу экрана!".
Кейс: Бесконечный скролл (Infinite Scroll) или ленивая загрузка (Lazy Load) картинок.
import { useEffect, useRef, useState } from 'react';
const LazyBox = () => {
const ref = useRef(null); // Ссылка на DOM-элемент
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
const element = ref.current;
if (!element) return;
// 1. Создаем наблюдателя
const observer = new IntersectionObserver((entries) => {
const entry = entries[0];
// entry.isIntersecting — true, если элемент появился во вьюпорте
if (entry.isIntersecting) {
setIsVisible(true);
console.log("Я появился!");
// Опционально: перестаем следить, если нужно сработать только 1 раз
observer.unobserve(element);
}
}, {
threshold: 0.5 // Сработает, когда 50% элемента будет видно
});
// 2. Начинаем следить за элементом
observer.observe(element);
// 3. Очистка (обязательно отключаем наблюдателя при удалении компонента)
return () => {
observer.disconnect();
};
}, []);
return (
<div style={{ height: '150vh' }}>
<p>Скролль вниз...</p>
{/* Элемент, за которым следим */}
<div
ref={ref}
style={{
marginTop: '100vh',
height: '200px',
background: isVisible ? 'green' : 'red',
transition: 'background 1s'
}}
>
{isVisible ? "Я ВИДИМ!" : "Я спрятан"}
</div>
</div>
);
};В CSS есть Media Queries (@media (max-width: 768px)), но они реагируют на ширину всего окна.
А что, если компонент хочет перестроиться, если изменилась ширина родительского контейнера (например, сайдбар сжался)? Это называется Container Queries, и в JS это делается через ResizeObserver.
const ResizableBox = () => {
const ref = useRef(null);
const [width, setWidth] = useState(0);
useEffect(() => {
const element = ref.current;
if (!element) return;
const observer = new ResizeObserver((entries) => {
// Получаем новые размеры
const newWidth = entries[0].contentRect.width;
setWidth(newWidth);
});
observer.observe(element);
return () => observer.disconnect();
}, []);
return (
<div className="container">
{/* Пользователь может менять размер этого div (через CSS resize) */}
<div ref={ref} style={{ resize: 'horizontal', overflow: 'auto', border: '1px solid black', padding: 20 }}>
<h3>Ширина: {Math.round(width)}px</h3>
{/* Адаптивная логика на уровне JS */}
{width < 300 ? (
<p>Compact View (Icon only)</p>
) : (
<p>Full View (Detailed text description...)</p>
)}
</div>
</div>
);
};Часто нужно слушать события не на кнопке, а "везде". Например:
- Нажатие
Escapeдля закрытия модалки. - Клик "вне" меню (Click Outside), чтобы закрыть его.
- Потеря сети (
offline).
Паттерн: Custom Hook. Вместо того чтобы писать useEffect в каждом компоненте, мы создаем свой хук.
**Пример: Хук useOnClickOutside**
import { useEffect } from "react";
// Хук принимает ref элемента (меню) и функцию закрытия (handler)
function useOnClickOutside(ref, handler) {
useEffect(() => {
const listener = (event) => {
// Если клик был внутри элемента — ничего не делаем
if (!ref.current || ref.current.contains(event.target)) {
return;
}
// Иначе вызываем закрытие
handler(event);
};
document.addEventListener("mousedown", listener);
document.addEventListener("touchstart", listener);
return () => {
document.removeEventListener("mousedown", listener);
document.removeEventListener("touchstart", listener);
};
}, [ref, handler]);
}
// Использование
// const menuRef = useRef();
// useOnClickOutside(menuRef, () => setIsOpen(false));localStorage и sessionStorage работают синхронно и только в браузере.
Это вызывает две проблемы:
- JSON parsing: Нужно постоянно делать
JSON.stringify/JSON.parse. - SSR (Next.js): На сервере нет
window, и код сlocalStorageупадет с ошибкой "window is not defined".
Пишем безопасный хук useLocalStorage:
import { useState, useEffect } from "react";
// key - ключ в сторадже, initialValue - дефолтное значение
function useLocalStorage(key, initialValue) {
// 1. Инициализация стейта (ленивая)
const [storedValue, setStoredValue] = useState(() => {
if (typeof window === "undefined") {
return initialValue; // Защита от SSR
}
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.log(error);
return initialValue;
}
});
// 2. Обновление стейта + запись в localStorage
const setValue = (value) => {
try {
// Поддержка functional update, как в useState
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
if (typeof window !== "undefined") {
window.localStorage.setItem(key, JSON.stringify(valueToStore));
}
} catch (error) {
console.log(error);
}
};
return [storedValue, setValue];
}
// Использование:
// const [theme, setTheme] = useLocalStorage("app-theme", "light");
// setTheme("dark"); // Автоматически сохранится в браузереЗдесь принцип тот же: используем useEffect для запроса прав и получения данных.
Пример доступа к геолокации:
const LocationComponent = () => {
const [location, setLocation] = useState(null);
const [error, setError] = useState(null);
const handleGetLocation = () => {
if (!navigator.geolocation) {
setError("Геолокация не поддерживается");
return;
}
navigator.geolocation.getCurrentPosition(
(position) => {
const { latitude, longitude } = position.coords;
setLocation({ latitude, longitude });
},
(err) => {
setError(err.message);
}
);
};
return (
<div>
<button onClick={handleGetLocation}>Где я?</button>
{location && <p>Lat: {location.latitude}, Lng: {location.longitude}</p>}
{error && <p style={{ color: 'red' }}>Error: {error}</p>}
</div>
);
};Классический (учебный) способ работы с инпутами в React называется Controlled Components.
// ❌ "Учебный" подход (Плохо для больших форм)
const SimpleForm = () => {
const [name, setName] = useState(""); // 1. Стейт для каждого поля
const [email, setEmail] = useState("");
// 2. При КАЖДОМ нажатии клавиши вызывается setName
// 3. Это вызывает РЕРЕНДЕР всего компонента SimpleForm
const handleChange = (e) => setName(e.target.value);
return <input value={name} onChange={handleChange} />;
};Почему это плохо для архитектора:
- Performance: Если у вас сложная форма и вы печатаете быстро, React делает ререндер на каждую букву. На слабых ноутбуках ввод начинает "лагать".
- Boilerplate: Для 20 полей вам нужно 20
useStateи 20 обработчиков.
Библиотека React Hook Form использует подход Uncontrolled Components. Она регистрирует инпуты через ref и не вызывает ререндер при вводе текста. Ререндер происходит, только если изменилась ошибка валидации или вы нажали "Submit".
Установка:
npm install react-hook-form
Основы RHF:
import { useForm } from "react-hook-form";
const LoginForm = () => {
// register — функция для привязки инпута
// handleSubmit — обертка, которая отменит preventDefault и соберет данные
// formState — объект с ошибками и состоянием
const { register, handleSubmit, formState: { errors } } = useForm();
const onSubmit = (data) => {
// data — это уже готовый JSON: { login: "...", password: "..." }
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
{/* 1. Регистрируем поле "login" */}
<input {...register("login", { required: true })} placeholder="Login" />
{/* Если есть ошибка — показываем */}
{errors.login && <span style={{color: 'red'}}>Это поле обязательно</span>}
{/* 2. Регистрируем поле "password" */}
<input {...register("password")} type="password" placeholder="Password" />
<button type="submit">Войти</button>
</form>
);
};Для Java-разработчика Zod — это аналог Hibernate Validator (Bean Validation).
Вместо того чтобы писать if (email.contains("@")) внутри кода компонента, мы описываем Схему данных отдельно.
Плюсы Zod:
- Single Source of Truth: Схема является и валидатором, и описанием типа TypeScript.
- Читаемость: Правила описаны декларативно.
Установка:
npm install zod @hookform/resolvers
Пример схемы:
import { z } from "zod";
// Описываем схему (как Java Class DTO)
const signUpSchema = z.object({
username: z.string().min(2, "Имя слишком короткое (мин 2)"),
email: z.string().email("Некорректный Email"),
age: z.number({ invalid_type_error: "Возраст должен быть числом" })
.min(18, "Вам должно быть 18+"),
password: z.string().min(6, "Пароль минимум 6 символов"),
confirmPassword: z.string()
}).refine((data) => data.password === data.confirmPassword, {
message: "Пароли не совпадают",
path: ["confirmPassword"], // Куда повесить ошибку
});Вот как выглядит современная форма регистрации. Мы используем zodResolver, чтобы подружить RHF и Zod.
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
// 1. Создаем схему
const schema = z.object({
email: z.string().email("Введите корректный email"),
password: z.string().min(6, "Минимум 6 символов"),
});
const AuthPage = () => {
// 2. Подключаем хук с резолвером
const {
register,
handleSubmit,
formState: { errors, isSubmitting } // isSubmitting — true, пока идет onSubmit
} = useForm({
resolver: zodResolver(schema),
mode: "onBlur" // Валидировать, когда пользователь убрал фокус с поля
});
// 3. Функция отправки (вызовется ТОЛЬКО если валидация прошла успешно)
const onSubmit = async (data) => {
// data гарантированно соответствует схеме
console.log("Отправка на сервер:", data);
// Эмулируем задержку сети
await new Promise(r => setTimeout(r, 2000));
alert("Успех!");
};
return (
<div className="form-container">
<h2>Регистрация</h2>
<form onSubmit={handleSubmit(onSubmit)}>
{/* Email Field */}
<div className="field">
<label>Email</label>
<input
{...register("email")}
className={errors.email ? "input-error" : ""}
/>
{errors.email && <p className="error">{errors.email.message}</p>}
</div>
{/* Password Field */}
<div className="field">
<label>Password</label>
<input
type="password"
{...register("password")}
className={errors.password ? "input-error" : ""}
/>
{errors.password && <p className="error">{errors.password.message}</p>}
</div>
{/* Кнопка блокируется во время отправки */}
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? "Загрузка..." : "Создать аккаунт"}
</button>
</form>
</div>
);
};
export default AuthPage;- Default Values: В
useFormвсегда передавайтеdefaultValues. Если этого не сделать, React будет ругаться на смену uncontrolled на controlled (input value будетundefinedпри старте).
useForm({ defaultValues: { email: "", password: "" } })- Интеграция с UI-библиотеками (MUI/AntD):
Поскольку UI-библиотеки часто используют свои сложные компоненты (например,
DatePicker), обычныйregister("...spread") туда не вставишь. Для этого в React Hook Form есть компонент Controller.
import { Controller } from "react-hook-form";
import Select from "react-select"; // Сторонняя библиотека
<Controller
name="city"
control={control} // берется из useForm()
render={({ field }) => (
// Мы вручную передаем value и onChange в кастомный компонент
<Select
{...field}
options={options}
/>
)}
/>npm install react-router-dom
В main.jsx (точка входа) нужно обернуть приложение в провайдер.
import { BrowserRouter } from "react-router-dom";
import App from "./App";
// BrowserRouter использует HTML5 History API
ReactDOM.createRoot(document.getElementById("root")).render(
<BrowserRouter>
<App />
</BrowserRouter>
);Теперь в App.jsx мы определяем таблицу маршрутизации.
import { Routes, Route } from "react-router-dom";
import HomePage from "./pages/Home";
import AboutPage from "./pages/About";
import NotFound from "./pages/NotFound";
function App() {
return (
<Routes>
{/* Точное совпадение не требуется, алгоритм v6 сам выберет лучший маршрут */}
<Route path="/" element={<HomePage />} />
<Route path="/about" element={<AboutPage />} />
{/* Wildcard * ловит все, что не совпало выше (404) */}
<Route path="*" element={<NotFound />} />
</Routes>
);
}Для Java-разработчика это важно.
- **Тег
<a href="/about">**: Убивает SPA. Браузер выгружает React из памяти, идет на сервер и качает всё заново. - **Компонент
<Link to="/about">**: Перехватывает клик, делаетevent.preventDefault(), меняет URL и говорит React Router'у: "Покажи нужный компонент".
import { Link, NavLink } from "react-router-dom";
const Navigation = () => (
<nav>
<Link to="/">Главная</Link>
{/* NavLink автоматически добавляет класс "active", если URL совпадает */}
<NavLink
to="/about"
className={({ isActive }) => isActive ? "red-link" : "blue-link"}
>
О нас
</NavLink>
</nav>
);Часто URL содержит ID сущности: /users/42.
Определение:
<Route path="/users/:id" element={<UserProfile />} />Использование (Хук useParams):
import { useParams } from "react-router-dom";
const UserProfile = () => {
// Достаем параметры из URL. Все параметры — строки!
const { id } = useParams();
// Дальше используем id для fetch запроса (см. главу 7)
// useEffect(() => fetch(`/api/users/${id}`), [id])
return <h1>Профиль пользователя ID: {id}</h1>;
};Это архитектурный паттерн. Как сделать так, чтобы Header и Sidebar были общими для всех страниц, а менялась только центральная часть?
В прошлом мы рендерили Header в каждом компоненте. В v6 мы используем Layouts и компонент <Outlet />.
Шаг 1. Создаем Layout-компонент
import { Outlet } from "react-router-dom";
const MainLayout = () => {
return (
<div className="app-grid">
<Sidebar />
<div className="content">
<Header />
{/* СЮДА будет подставляться дочерний роут */}
<main>
<Outlet />
</main>
<Footer />
</div>
</div>
);
};Шаг 2. Настраиваем вложенность в App
<Routes>
{/* Родительский роут с Layout */}
<Route path="/" element={<MainLayout />}>
{/* index — это то, что покажется по пути "/" внутри Outlet */}
<Route index element={<Dashboard />} />
<Route path="users" element={<UsersList />} />
<Route path="settings" element={<Settings />} />
</Route>
{/* Маршрут БЕЗ Layout (например, страница логина) */}
<Route path="/login" element={<LoginPage />} />
</Routes>Это позволяет очень гибко управлять слоями приложения (AdminLayout, UserLayout, GuestLayout).
Иногда нужно перенаправить пользователя не по клику, а после действия (например, после успешного логина или сохранения формы).
import { useNavigate } from "react-router-dom";
const LoginPage = () => {
const navigate = useNavigate();
const handleLogin = async () => {
await authService.login();
// Перенаправление на главную
// { replace: true } заменяет текущую запись в истории (кнопка "Назад" не вернет на логин)
navigate("/", { replace: true });
// Или на шаг назад
// navigate(-1);
};
return <button onClick={handleLogin}>Login</button>;
};Как запретить доступ к /dashboard неавторизованным юзерам?
В Java мы используем Security Filters. В React мы используем Component Wrappers.
Создаем компонент-обертку:
import { Navigate, useLocation } from "react-router-dom";
// Принимаем children (то, что внутри)
const RequireAuth = ({ children }) => {
const user = useAuth(); // Представим, что у нас есть хук авторизации (см. контекст)
const location = useLocation();
if (!user) {
// Если нет юзера, редиректим на логин.
// state={{ from: location }} нужен, чтобы после логина вернуть юзера туда, куда он хотел попасть.
return <Navigate to="/login" state={{ from: location }} replace />;
}
return children;
};Применяем в роутах:
<Route
path="/dashboard"
element={
<RequireAuth>
<Dashboard />
</RequireAuth>
}
/>Давайте посмотрим, как мы делали запросы в Главе 7.
// ❌ Ручной менеджмент состояния (Boilerplate)
const UserProfile = ({ userId }) => {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let isMounted = true; // Защита от установки стейта в размонтированный компонент
setIsLoading(true);
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(d => {
if (isMounted) {
setData(d);
setIsLoading(false);
}
})
.catch(e => {
if (isMounted) {
setError(e);
setIsLoading(false);
}
});
return () => { isMounted = false; };
}, [userId]);
if (isLoading) return "Loading...";
if (error) return "Error";
return <div>{data.name}</div>;
};Проблемы этого подхода для Архитектора:
- Нет кэширования: Если вы скроете компонент и покажете снова — запрос полетит опять.
- Нет дедупликации: Если 5 компонентов на странице просят данные юзера — полетит 5 одинаковых запросов.
- Race Conditions: Если
userIdбыстро поменяется с 1 на 2, а потом обратно на 1, вы не гарантируете, в каком порядке придут ответы. - Сложность обновления: Как обновить эти данные после POST-запроса?
TanStack Query — это асинхронный менеджер состояния.
Аналогия для Java-разработчика: Представьте, что это Spring @Cacheable + Hibernate L2 Cache, но прямо в браузере.
Установка:
npm install @tanstack/react-query
Нужно обернуть приложение в QueryClientProvider (обычно в main.jsx):
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient();
ReactDOM.createRoot(document.getElementById('root')).render(
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
);Вместо useEffect мы используем хук useQuery.
Ключевые понятия:
- Query Key (Ключ запроса): Уникальный массив (как ключ в HashMap), по которому кэшируются данные.
['users', 1]. - Query Function (Функция запроса): Любая асинхронная функция, которая возвращает данные или кидает ошибку.
import { useQuery } from '@tanstack/react-query';
// Функция-фетчер (вынесена отдельно)
const fetchUser = async (id) => {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error('Network error');
return res.json();
};
const UserProfile = ({ userId }) => {
const { data, isLoading, isError, error } = useQuery({
// Ключ: зависит от userId. Если userId сменится, React Query сам сделает перезапрос
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
// Опции (Architectural decisions):
staleTime: 1000 * 60 * 5, // Данные считаются "свежими" 5 минут (не делать повторных запросов)
retry: 1, // Количество попыток при ошибке
});
if (isLoading) return <span>Загрузка...</span>;
if (isError) return <span>Ошибка: {error.message}</span>;
return (
<div>
<h1>{data.name}</h1>
<p>{data.email}</p>
</div>
);
};Что мы получили бесплатно:
- Loading/Error states: Не нужно создавать
useState. - Auto Refetch: Если вы переключите вкладку браузера и вернетесь назад, React Query автоматически обновит данные (Window Focus Refetching).
- Deduping: Если этот компонент отрендерить 10 раз, на сервер уйдет только 1 запрос.
Это то, что делает UX "мгновенным". React Query использует стратегию Stale-While-Revalidate.
- Fresh (Свежие): Данные только что загружены. Пока они Fresh, запросы на сервер не идут, данные берутся из кэша.
- Stale (Протухшие): Время
staleTimeистекло. Данные все еще доступны в кэше, но React Query при следующем обращении сделает фоновый запрос (background refetch), чтобы обновить их. Пользователь видит старые данные, а через секунду они незаметно обновляются. - Inactive: Если компонент удален с экрана, данные лежат в кэше
gcTime(по дефолту 5 минут). Если компонент вернется — данные покажутся мгновенно.
Для POST/PUT/DELETE запросов используется useMutation.
Главный паттерн здесь: "Изменил данные -> Инвалидируй кэш".
Если мы добавили новую задачу в список, наш кэш списка задач стал неактуальным. Мы должны сказать React Query: "Пометь ключ ['todos'] как грязный".
import { useMutation, useQueryClient } from '@tanstack/react-query';
const CreateTodo = () => {
const queryClient = useQueryClient(); // Доступ к кэшу
const mutation = useMutation({
mutationFn: (newTodo) => {
return fetch('/api/todos', {
method: 'POST',
body: JSON.stringify(newTodo),
});
},
// Callback при успехе
onSuccess: () => {
// Самая важная строка!
// Мы говорим: данные по ключу ['todos'] устарели.
// React Query АВТОМАТИЧЕСКИ сделает GET запрос списка задач и обновит интерфейс.
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
return (
<button
onClick={() => mutation.mutate({ title: "Выучить React Query" })}
disabled={mutation.isPending}
>
{mutation.isPending ? "Добавляю..." : "Добавить задачу"}
</button>
);
};Пользователи ненавидят ждать. Оптимистичное обновление — это когда мы обновляем UI до того, как сервер ответил "ОК". Мы верим, что всё будет хорошо.
Алгоритм:
- Пользователь нажал "Лайк".
- Мы мгновенно меняем цвет сердечка и счетчик (+1) в кэше вручную.
- Отправляем запрос на сервер.
- Если сервер ответил ошибкой — откатываем изменения назад (Rollback).
const mutation = useMutation({
mutationFn: updateTodo,
onMutate: async (newTodo) => {
// 1. Отменяем исходящие рефетчи, чтобы они не перезаписали наше оптимистичное обновление
await queryClient.cancelQueries({ queryKey: ['todos'] });
// 2. Сохраняем предыдущий стейт (для отката)
const previousTodos = queryClient.getQueryData(['todos']);
// 3. Оптимистично обновляем кэш
queryClient.setQueryData(['todos'], (old) => [...old, newTodo]);
// Возвращаем контекст для onError
return { previousTodos };
},
onError: (err, newTodo, context) => {
// 4. Если ошибка — возвращаем как было
queryClient.setQueryData(['todos'], context.previousTodos);
},
onSettled: () => {
// 5. В любом случае (успех или ошибка) делаем честный запрос на сервер,
// чтобы убедиться в синхронизации
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});Прежде чем тащить Redux, опытный архитектор проверяет: "А нужен ли он?". Проблема Prop Drilling — это когда вы передаете данные через компоненты, которым эти данные не нужны.
// ❌ Prop Drilling
// App -> Layout -> Header -> UserMenu -> Avatar
// Layout и Header просто передают user вниз, не используя его.
const App = () => {
const [user, setUser] = useState({ name: "Alex" });
return <Layout user={user} />;
};Решение: Композиция компонентов (Component Composition) Вместо передачи данных, мы передаем сами компоненты.
// ✅ Composition
const App = () => {
const [user, setUser] = useState({ name: "Alex" });
// Мы создаем Avatar здесь, где есть user
// И передаем его как prop (или children)
return (
<Layout>
<Header>
<UserMenu>
<Avatar user={user} />
</UserMenu>
</Header>
</Layout>
);
};Если композиция решает проблему, глобальный стейт не нужен.
useContext встроен в React. Это идеальный инструмент для редко меняющихся данных (Тема, Локализация, Текущий пользователь).
Ментальная ловушка: Context — это не совсем стейт-менеджер, это механизм Dependency Injection (внедрения зависимостей).
Проблема производительности: Если контекст обновится, ВСЕ компоненты, использующие этот контекст, перерисуются. Поэтому он плох для часто меняющихся данных (например, координаты мыши).
Пример (ThemeContext):
import { createContext, useContext, useState } from "react";
// 1. Создаем контекст (с дефолтным значением)
const ThemeContext = createContext(null);
// 2. Создаем Провайдер (компонент-обертка)
export const ThemeProvider = ({ children }) => {
const [theme, setTheme] = useState("light");
const toggleTheme = () => {
setTheme((prev) => (prev === "light" ? "dark" : "light"));
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};
// 3. Создаем кастомный хук (для удобства)
export const useTheme = () => {
const context = useContext(ThemeContext);
if (!context) throw new Error("useTheme must be used within ThemeProvider");
return context;
};
// 4. Использование в компоненте
// const { theme, toggleTheme } = useTheme();Если вы идете работать в крупную компанию (Банк, Телеком), там будет Redux. Раньше Redux был многословен (switch-case, константы). Сейчас есть Redux Toolkit (RTK) — это официальная, упрощенная обертка.
Основные понятия:
- Store: Единое хранилище всего стейта приложения.
- Slice: Кусочек логики (например,
authSlice,cartSlice). Содержит и состояние, и редюсеры (функции изменения). - Dispatch: Отправка события ("Я хочу изменить стейт").
- Selector: Чтение данных из стейта.
Пример (Счетчик на RTK):
1. Создаем Slice (features/counter/counterSlice.js):
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
// В RTK можно писать мутабельный код (state.value += 1)!
// Библиотека Immer под капотом сделает это иммутабельным.
increment: (state) => {
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
incrementByAmount: (state, action) => {
state.value += action.payload;
},
},
});
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;2. Настраиваем Store (store.js):
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './features/counter/counterSlice';
export const store = configureStore({
reducer: {
counter: counterReducer, // Подключаем слайсы
},
});3. Использование в компоненте:
import { useSelector, useDispatch } from 'react-redux';
import { increment } from './features/counter/counterSlice';
const Counter = () => {
// Чтение (автоматическая подписка на обновления)
const count = useSelector((state) => state.counter.value);
// Инструмент для вызова действий
const dispatch = useDispatch();
return (
<button onClick={() => dispatch(increment())}>
Count is {count}
</button>
);
};Вердикт: Мощно, есть DevTools (путешествие во времени), но много шаблонного кода (Boilerplate).
Zustand (с нем. "Состояние") — это легкая альтернатива Redux.
- Нет Провайдеров (
<Provider>). - Работает на хуках.
- Минимум кода.
- Идеально работает с React вне компонентов (можно вызывать из обычных JS-файлов).
Установка: npm install zustand
Пример (тот же счетчик, но на Zustand):
import { create } from 'zustand';
// Создаем стор (хук)
const useStore = create((set) => ({
// State
count: 0,
// Actions (методы изменения)
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
}));
// Использование в компоненте
const Counter = () => {
// Выбираем только то, что нужно (селектор)
const count = useStore((state) => state.count);
const increment = useStore((state) => state.increment);
return <button onClick={increment}>{count}</button>;
};Для архитектора: Zustand — лучший выбор для новых проектов среднего размера, где Redux избыточен.
Это контракт компонента. В Java вы описываете аргументы метода, в React — интерфейс пропсов.
Interface vs Type:
- Interface: Ближе к Java. Можно расширять (
extends). Хорошо для библиотек. - Type: Более гибкий (Union types, Tuples). В React-сообществе для пропсов чаще используют
type, но это дело вкуса.
// Описываем контракт
type ButtonProps = {
label: string;
onClick: () => void; // Функция, ничего не возвращает
variant?: 'primary' | 'secondary'; // ? - необязательное поле (Union type)
isDisabled?: boolean;
style?: React.CSSProperties; // Встроенный тип для inline-стилей
};
// Применяем к компоненту
const Button = ({
label,
onClick,
variant = 'primary', // Значение по умолчанию
isDisabled
}: ButtonProps) => { // TS проверит, что деструктуризация соответствует типу
return (
<button
onClick={onClick}
disabled={isDisabled}
className={`btn-${variant}`}
>
{label}
</button>
);
};Использование:
Если вы попробуете написать <Button label={123} />, IDE (VS Code) подчеркнет это красным еще до запуска.
TS часто умеет выводить тип сам (Inference).
// ✅ TS сам поймет, что count - это number
const [count, setCount] = useState(0); Но если начальное значение null или пустой массив, нужно помочь компилятору через Generics:
type User = { id: number; name: string };
// ❌ Без дженерика user будет типа "null" и вы не сможете присвоить туда объект
// const [user, setUser] = useState(null);
// ✅ Явно указываем: тут будет User ИЛИ null
const [user, setUser] = useState<User | null>(null);
// Теперь TS заставит делать проверку перед обращением
// user.name — Ошибка: "Object is possibly null"
// user?.name — ОКЗдесь есть нюанс: ссылка на DOM-элемент всегда должна учитывать null (пока элемент не отрисовался).
// Ссылка на Input
const inputRef = useRef<HTMLInputElement>(null);
const focusInput = () => {
// TS требует проверки на null (?. или if)
inputRef.current?.focus();
};Самая большая боль новичков: "Что писать вместо any в e?"
В React есть свои обертки над нативными событиями.
import { useState, ChangeEvent, FormEvent, MouseEvent } from 'react';
const Form = () => {
const [value, setValue] = useState('');
// 1. Событие изменения инпута
// ChangeEvent принимает дженерик (какой элемент вызвал событие)
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
};
// 2. Событие отправки формы
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
console.log(value);
};
// 3. Событие клика мыши
const handleClick = (e: MouseEvent<HTMLButtonElement>) => {
console.log("Clicked at", e.clientX, e.clientY);
};
return (
<form onSubmit={handleSubmit}>
<input type="text" onChange={handleChange} />
<button onClick={handleClick}>Submit</button>
</form>
);
};Лайфхак: Если не знаете тип события, наведите курсор на проп onChange в JSX, и IDE подскажет правильный тип.
Если компонент принимает вложенный контент, используйте тип React.ReactNode. Это объединение всего, что React может нарисовать (JSX, string, number, null, array).
type CardProps = {
title: string;
children: React.ReactNode; // Стандартный тип для children
};
const Card = ({ title, children }: CardProps) => (
<div className="card">
<h1>{title}</h1>
{children}
</div>
);Представьте, что вы пишете универсальный список List, который может принимать массив строк, чисел или объектов User.
type ListProps<T> = {
items: T[];
renderItem: (item: T) => React.ReactNode;
};
// Объявляем Generic параметр <T>
// extends unknown (или any) нужно, чтобы парсер JSX не перепутал <T> с тегом
const List = <T extends unknown>({ items, renderItem }: ListProps<T>) => {
return (
<ul>
{items.map((item, index) => (
<li key={index}>{renderItem(item)}</li>
))}
</ul>
);
};
// Использование:
// TS сам поймет, что T = string
<List
items={["a", "b", "c"]}
renderItem={(item) => <strong>{item.toUpperCase()}</strong>}
/>Если вы используете Vite, то Vitest — это ваш выбор.
- Он использует тот же конфиг, что и Vite.
- Он быстрее Jest.
- API совместимо с Jest (describe, it, expect).
Настройка:
npm install -D vitest jsdom @testing-library/react
Это философия тестирования UI.
Старый подход (Enzyme): Тестировали реализацию. "Проверь, что в стейте count === 1".
Новый подход (RTL): Тестируем поведение. "Нажми на кнопку и проверь, что на экране появился текст '1'".
Пользователю все равно, что у вас в стейте или в Redux. Ему важно, что он видит на экране.
Пример теста компонента Counter:
// Counter.test.jsx
import { render, screen, fireEvent } from '@testing-library/react';
import Counter from './Counter';
import { describe, it, expect } from 'vitest';
describe('Counter Component', () => {
it('incrementa счетчик при клике', () => {
// 1. Рендерим компонент в виртуальном DOM
render(<Counter />);
// 2. Ищем элементы (как пользователь)
const button = screen.getByText(/count is/i); // RegExp (case insensitive)
// Проверяем начальное состояние
expect(button).toHaveTextContent('count is 0');
// 3. Совершаем действие
fireEvent.click(button);
// 4. Проверяем результат
expect(button).toHaveTextContent('count is 1');
});
});Как тестировать компоненты, которые делают запросы (useQuery), не поднимая реальный бэкенд?
MSW перехватывает запросы на сетевом уровне (через Service Worker в браузере или через Node interceptor в тестах).
Это лучше, чем мокать fetch глобально, так как вы описываете "игрушечный сервер".
// mocks/handlers.js
import { http, HttpResponse } from 'msw'
export const handlers = [
// Перехватываем GET /api/user
http.get('/api/user', () => {
return HttpResponse.json({
name: 'John Maverick',
})
}),
]В тесте компонент сделает "реальный" фетч, но MSW отдаст ему JSON выше.
Unit-тесты проверяют компоненты изолированно. E2E (End-to-End) проверяют весь сценарий: "Открыл браузер -> Залогинился -> Купил товар".
Cypress был королем, но сейчас Playwright (от Microsoft) побеждает. Он быстрее, поддерживает несколько вкладок и параллелизацию.
// example.spec.js (Playwright)
import { test, expect } from '@playwright/test';
test('пользователь может войти', async ({ page }) => {
await page.goto('http://localhost:5173/login');
await page.fill('input[name="email"]', 'user@example.com');
await page.fill('input[name="password"]', 'password');
await page.click('button[type="submit"]');
// Ждем, пока URL изменится на dashboard
await expect(page).toHaveURL(/.*dashboard/);
});Это самая распространенная уязвимость на фронтенде. Злоумышленник пытается внедрить свой JS-код в вашу страницу, чтобы украсть куки, токены или данные формы.
React защищает вас по умолчанию. React автоматически экранирует (escapes) все переменные перед рендерингом.
const maliciousInput = "<img src=x onerror=alert('Hacked!') />";
// React превратит это в безопасную строку текста, скрипт НЕ выполнится.
return <div>{maliciousInput}</div>; **Дыра в безопасности: dangerouslySetInnerHTML**
Иногда нам НУЖНО вставить HTML (например, контент из CMS или отрендеренный Markdown). React заставляет вас писать длинное и страшное название пропса, чтобы вы понимали риск.
Решение: Санитизация (Sanitization) Никогда не вставляйте "сырой" HTML. Используйте библиотеку DOMPurify.
import DOMPurify from 'dompurify';
const BlogPost = ({ content }) => {
// content = "<script>stealCookies()</script><p>Hello</p>"
// DOMPurify вырежет <script>, но оставит безопасные теги (<p>, <b>)
const cleanContent = DOMPurify.sanitize(content);
return (
<div dangerouslySetInnerHTML={{ __html: cleanContent }} />
);
};Это классический архитектурный холивар. Где хранить JWT (Access Token)?
- LocalStorage / SessionStorage:
- Плюс: Легко работать (
localStorage.getItem('token')). - Минус (Критический): Уязвимо к XSS. Любой JS-код на странице (в том числе вредоносный скрипт из npm-пакета, который вы случайно подключили) имеет доступ к
localStorage.
- HttpOnly Cookies:
- Плюс: JS не имеет доступа к этим кукам. Даже если на сайте есть XSS-уязвимость, скрипт хакера не сможет прочитать токен. Браузер сам отправляет куку с каждым запросом.
- Минус: Нужно настраивать на бэкенде. Сложнее работать на мобильных клиентах.
Вердикт Архитектора:
Для приложений с чувствительными данными (FinTech, Enterprise) — только httpOnly Cookies.
Access Token хранится в куке, Refresh Token — тоже в (другой) куке.
localStorage допустим только для некритичных данных или если вы на 100% уверены в отсутствии XSS (что невозможно гарантировать).
Если мы используем Cookies, появляется новая угроза. Злоумышленник может создать сайт-ловушку, который отправит POST-запрос на ваш банк api.bank.com/transfer. Браузер пользователя любезно прикрепит ваши банковские куки к этому запросу, и сервер его выполнит.
Защита:
- SameSite Cookie Attribute: Установите атрибут
SameSite=Strict(илиLax). Это запрещает браузеру отправлять куки, если запрос инициирован с другого домена. - CSRF Token: (Как в Spring Security). Сервер выдает уникальный токен в заголовке, и фронтенд должен возвращать его с каждым мутирующим запросом.
Современный фронтенд — это 1000+ зависимостей в node_modules.
Если злоумышленник захватит популярную библиотеку (например, "левый пад") и добавит туда майнер или стилеры паролей, ваш билд заразится.
Меры:
npm audit— регулярно проверяйте уязвимости.- Фиксация версий в
package-lock.json.
Итог по Главе 15: Безопасность — это многослойный пирог. React дает первый слой (экранирование), DOMPurify — второй, httpOnly Cookies — третий.
Чтобы оптимизировать, надо понять, как оно работает. С React 16 движок называется Fiber.
- Render Phase (Асинхронная): React вычисляет изменения. Он может поставить этот процесс на паузу, если браузеру нужно отрисовать кадр анимации (Time Slicing).
- Commit Phase (Синхронная): React применяет изменения к DOM.
Проблема: Если вы делаете тяжелые вычисления прямо в теле компонента, вы блокируете Render Phase.
Мемоизация — это кэширование результатов. Внимание: Не мемоизируйте всё подряд! Сама мемоизация тоже тратит ресурсы.
1. useMemo (Для тяжелых вычислений):
const ExpensiveComponent = ({ list }) => {
// ❌ Плохо: filter и sort запускаются при КАЖДОМ рендере (даже если list не менялся)
// const sorted = list.filter(i => i.active).sort((a, b) => a.id - b.id);
// ✅ Хорошо: пересчитываем, только если изменился list
const sorted = useMemo(() => {
console.log("Тяжелая фильтрация...");
return list.filter(i => i.active).sort((a, b) => a.id - b.id);
}, [list]);
return <div>{/*...*/}</div>;
};2. React.memo (Для компонентов):
Оборачивает компонент и предотвращает ререндер, если пропсы не изменились.
Полезно для огромных списков или графиков.
const Child = React.memo(({ name }) => {
console.log("Child render"); // Не вызовется, если name тот же
return <div>{name}</div>;
});3. useCallback (Для функций):
Важнейший момент для ссылочной целостности.
const Parent = () => {
const [count, setCount] = useState(0);
// ❌ При каждом рендере Parent создается НОВАЯ функция handleClick.
// Ссылка меняется -> Child (даже обернутый в memo) думает, что пропсы изменились -> РЕРЕНДЕР.
// const handleClick = () => console.log('Click');
// ✅ Ссылка на функцию сохраняется между рендерами.
const handleClick = useCallback(() => {
console.log('Click');
}, []);
return (
<>
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
<Child onClick={handleClick} />
</>
);
};SPA загружает весь JS сразу (bundle.js может весить 5MB). Пользователь ждет белый экран. Решение: Разбить код на куски (Chunks) и грузить их по требованию.
Используем React.lazy и <Suspense>.
import React, { Suspense } from 'react';
// Этот компонент не попадет в основной бандл!
// Он загрузится отдельным файлом только тогда, когда мы попытаемся его отрисовать.
const AdminPanel = React.lazy(() => import('./pages/AdminPanel'));
const App = () => {
return (
<div>
<Header />
{/* Suspense показывает fallback, пока грузится JS-файл админки */}
<Suspense fallback={<div>Загрузка админки...</div>}>
<AdminPanel />
</Suspense>
</div>
);
};Обычно это делают на уровне Роутера: каждая страница — отдельный чанк.
Если нужно отрисовать 10,000 строк в таблице, DOM "умрет". Браузер не потянет столько div-ов. Решение: Рендерить только то, что сейчас во вьюпорте (видимой области) + небольшой запас сверху и снизу.
Библиотеки: react-window или react-virtuoso.
import { FixedSizeList as List } from 'react-window';
const Row = ({ index, style }) => (
<div style={style}>Row {index}</div>
);
const Example = () => (
<List
height={150} // Высота окна просмотра
itemCount={10000} // Количество элементов
itemSize={35} // Высота одной строки
width={300}
>
{Row}
</List>
);Результат: В DOM всегда будет только ~10 элементов, даже если скроллить список из миллиона.
Custom Hook — это обычная JS-функция, которая:
- Начинается с
use(обязательно!). - Внутри себя вызывает другие хуки (
useState,useEffect).
Это главный механизм переиспользования логики в React.
**Пример: useDebounce**
Задача: не отправлять запрос на сервер при каждом нажатии клавиши в поиске, а подождать, пока пользователь закончит печатать.
import { useState, useEffect } from 'react';
// Хук принимает значение и задержку
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
// Устанавливаем таймер
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
// Очистка: если value изменится до истечения таймера (пользователь нажал клавишу),
// предыдущий таймер сбросится.
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
// Использование в компоненте
const SearchBox = () => {
const [text, setText] = useState('');
// API запрос полетит только если text не менялся 500мс
const debouncedText = useDebounce(text, 500);
useEffect(() => {
if (debouncedText) {
console.log('API Request:', debouncedText);
}
}, [debouncedText]);
return <input onChange={(e) => setText(e.target.value)} />;
};Этот паттерн используется в библиотеках типа Radix UI или Headless UI.
Идея в том, чтобы разбить сложный компонент на несколько частей, которые неявно делят состояние через Context, но дают гибкость в верстке пользователю.
**Пример: <Tabs>**
Мы хотим такой API:
<Tabs>
<TabList>
<Tab>One</Tab>
<Tab>Two</Tab>
</TabList>
<TabPanels>
<Panel>Content 1</Panel>
<Panel>Content 2</Panel>
</TabPanels>
</Tabs>Реализация:
import React, { createContext, useContext, useState } from "react";
// 1. Создаем контекст для общения между родителем и детьми
const TabsContext = createContext();
// 2. Родительский компонент держит стейт
const Tabs = ({ children, defaultIndex = 0 }) => {
const [selectedIndex, setSelectedIndex] = useState(defaultIndex);
return (
<TabsContext.Provider value={{ selectedIndex, setSelectedIndex }}>
<div className="tabs-root">{children}</div>
</TabsContext.Provider>
);
};
// 3. Дочерние компоненты потребляют контекст
const Tab = ({ children, index }) => {
const { selectedIndex, setSelectedIndex } = useContext(TabsContext);
const isActive = index === selectedIndex;
return (
<button
className={isActive ? "tab active" : "tab"}
onClick={() => setSelectedIndex(index)}
>
{children}
</button>
);
};
const Panel = ({ children, index }) => {
const { selectedIndex } = useContext(TabsContext);
if (index !== selectedIndex) return null;
return <div className="panel">{children}</div>;
};
// Экспортируем как свойства (необязательно, но стильно)
Tabs.Tab = Tab;
Tabs.Panel = Panel;До хуков это были единственные способы шарить логику. Сейчас они встречаются реже, но знать их нужно.
1. Higher-Order Components (HOC) Функция, которая принимает компонент и возвращает новый компонент с добавленными пропсами. Аналогия: Декораторы в Python или Java.
// HOC withAuth
const withAuth = (Component) => {
return (props) => {
const isAuthenticated = checkAuth(); // какая-то логика
if (!isAuthenticated) return <Login />;
// Пробрасываем пропсы дальше + добавляем user
return <Component {...props} user={{ name: "User" }} />;
};
};
// Использование
const Profile = ({ user }) => <div>Profile of {user.name}</div>;
export default withAuth(Profile);Минусы: "Wrapper Hell" (ад оберток) в DevTools, коллизии имен пропсов.
2. Render Props
Компонент принимает функцию, возвращающую JSX, в качестве пропса (обычно render или children).
const MouseTracker = ({ render }) => {
const [pos, setPos] = useState({ x: 0, y: 0 });
// Логика слежения за мышью...
// Мы не знаем, что рендерить, отдаем координаты наружу
return render(pos);
};
// Использование
<MouseTracker
render={({ x, y }) => (
<h1>Мышь на {x}, {y}</h1>
)}
/>Где живет сейчас: React Hook Form (<Controller render={...} />), виртуализация списков.
Если ваш useState превращается в спагетти (много флагов isLoading, isError, data, которые зависят друг от друга), пора брать useReducer.
Это "Redux на минималках" внутри одного компонента.
import { useReducer } from 'react';
// 1. Reducer - чистая функция (state, action) => newState
const initialState = { count: 0, error: null };
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + 1, error: null };
case 'decrement':
if (state.count <= 0) return { ...state, error: "Нельзя меньше 0" };
return { ...state, count: state.count - 1 };
default:
throw new Error();
}
}
// 2. Использование
const Counter = () => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
{state.error && <div style={{color: 'red'}}>{state.error}</div>}
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
</>
);
};Иногда нужно отрендерить элемент (Модальное окно, Тултип, Дропдаун) визуально поверх всего интерфейса, чтобы z-index или overflow: hidden родителя не обрезали его.
createPortal позволяет рендерить дочерний элемент в другой узел DOM (обычно в document.body), но логически оставлять его в дереве React (события всплывают как обычно!).
import { createPortal } from 'react-dom';
const Modal = ({ isOpen, onClose, children }) => {
if (!isOpen) return null;
// Рендерим в body, а не внутрь родительского div
return createPortal(
<div className="modal-overlay">
<div className="modal-content">
<button onClick={onClose}>Close</button>
{children}
</div>
</div>,
document.body // Целевой узел DOM
);
};
// Использование
// <Modal isOpen={open}><h1>Привет из портала!</h1></Modal>Компонент — это класс, наследуемый от React.Component.
import React, { Component } from 'react';
// Похоже на Java, верно?
class Counter extends Component {
// 1. Конструктор (инициализация)
constructor(props) {
super(props); // Обязательный вызов родительского конструктора
// Стейт ВСЕГДА должен быть объектом
this.state = {
count: 0,
lastAction: null
};
}
// 2. Метод (обработчик)
increment() {
// Внимание: здесь будет проблема с this (см. п. 18.4)
this.setState({ count: this.state.count + 1 });
}
// 3. Метод рендеринга (обязательный)
render() {
// Деструктуризация стейта и пропсов для чистоты
const { count } = this.state;
const { label } = this.props;
return (
<div>
<h1>{label}: {count}</h1>
{/* Обратите внимание на this.increment */}
<button onClick={() => this.increment()}>+</button>
</div>
);
}
}В отличие от useState, где мы дробим стейт на кусочки, здесь стейт — это один большой объект.
Главное отличие: Автоматическое слияние (Merging)
Когда вы вызываете this.setState, React сливает ваш объект с текущим стейтом (поверхностно).
// Исходный стейт: { count: 0, username: "Dima" }
this.setState({ count: 5 });
// Итоговый стейт: { count: 5, username: "Dima" }
// Поле username не исчезло!В хуке useState объект заменился бы целиком, и username пропал бы.
У класса есть четкие этапы жизни. Это именно то, что заменяет useEffect.
**1. componentDidMount()**
Вызывается один раз после того, как компонент появился в DOM.
Аналог: useEffect(() => { ... }, [])
Использование: Запросы к API, подписки.
**2. componentDidUpdate(prevProps, prevState)**
Вызывается после каждого обновления (пропсов или стейта).
Аналог: useEffect(() => { ... }, [prop])
Важно: Всегда оборачивайте логику в if, иначе получите бесконечный цикл.
componentDidUpdate(prevProps) {
// Реагируем только если изменился userId
if (this.props.userId !== prevProps.userId) {
this.fetchData(this.props.userId);
}
}**3. componentWillUnmount()**
Вызывается перед удалением компонента.
Аналог: useEffect(() => { return () => ... }, [])
Использование: Очистка таймеров, отписка от событий.
Это то, что сводит с ума людей, пришедших из Java/C#.
В JavaScript контекст this зависит от того, как вызвана функция, а не где она объявлена. При передаче метода в onClick контекст теряется.
class Button extends Component {
handleClick() {
console.log(this); // undefined (в строгом режиме)
}
render() {
// ❌ Ошибка при клике: this is undefined
return <button onClick={this.handleClick}>Click</button>;
}
}Решение 1: Bind в конструкторе (Старая школа)
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this); // Жестко привязываем контекст
}Решение 2: Arrow Functions (Class Fields) — Современный стандарт
Стрелочные функции не имеют своего this, они берут его из окружения (экземпляра класса).
class Button extends Component {
// ✅ Пишем методы как стрелочные функции-свойства
handleClick = () => {
console.log(this.state); // Все работает
}
}Это единственная причина использовать классы сегодня. Если в компоненте произойдет JS-ошибка (throw Error), все React-приложение упадет (белый экран). Error Boundary позволяет поймать ошибку и показать красивый "Oops, something went wrong", не роняя весь интерфейс.
class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
// Специальный метод жизненного цикла
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, info) {
// Можно отправить лог ошибки в Sentry/Datadog
logErrorToService(error, info);
}
render() {
if (this.state.hasError) {
return <h1>Что-то пошло не так.</h1>;
}
return this.props.children;
}
}
// Использование:
// <ErrorBoundary>
// <DangerousComponent />
// </ErrorBoundary>С 13-й версии Next.js представил App Router. Теперь структура папок определяет URL вашего сайта.
Структура проекта:
app/
├── layout.tsx # Общий Layout (HTML, Body)
├── page.tsx # Главная страница (/)
├── about/
│ └── page.tsx # Страница /about
└── blog/
├── layout.tsx # Layout только для блога (например, сайдбар)
├── page.tsx # Страница /blog
└── [slug]/ # Динамический сегмент
└── page.tsx # Страница /blog/post-1
Пример app/page.tsx:
В Next.js все компоненты по умолчанию — Серверные (Server Components). Это значит, что они рендерятся на Node.js сервере, превращаются в HTML и отправляются браузеру.
// app/page.tsx
// Это React-компонент, но он выполняется НА СЕРВЕРЕ.
export default function Home() {
return (
<main>
<h1>Привет, Next.js</h1>
<p>Этот текст прилетел в браузер уже готовым HTML.</p>
</main>
);
}Для архитектора важно понимать, когда и где собирается страница.
То, к чему мы привыкли. Рендеринг в браузере. В Next.js нужно явно указать директиву 'use client' в начале файла, если вам нужны useState, useEffect или обработчики событий (onClick).
'use client'; // Обязательно для интерактивности
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}Страница собирается на сервере при каждом запросе. Аналог: JSP или Thymeleaf. Когда нужно: Персонализированные данные (Dashboard, лента новостей), которые меняются каждую секунду.
// По умолчанию Next.js пытается кэшировать, но можно отключить кэш
async function getData() {
const res = await fetch('https://api.example.com/data', {
cache: 'no-store' // Никогда не кэшировать -> SSR
});
return res.json();
}
export default async function Page() {
const data = await getData();
return <main>Data: {data.value}</main>;
}Страница собирается один раз во время билда (npm run build). Сервер отдает готовый HTML-файл. Это молниеносно быстро (может раздаваться через CDN).
Аналог: Генерация статических отчетов.
Когда нужно: Блог, документация, лендинг, "О нас".
// Если fetch не имеет опций, Next.js по умолчанию считает это SSG
async function getPosts() {
const res = await fetch('https://api.example.com/posts');
return res.json();
}Страница статическая (быстрая), но раз в N секунд сервер в фоне пересобирает её, если зашел пользователь. Киллер-фича Next.js.
async function getData() {
const res = await fetch('https://api.example.com/news', {
next: { revalidate: 60 } // Обновлять кэш не чаще чем раз в 60 секунд
});
return res.json();
}Google Core Web Vitals (метрики качества сайта) наказывают за "сдвиги макета" (CLS), когда картинка загружается и толкает текст вниз.
**1. next/image**
Компонент <Image /> автоматически:
- Конвертирует картинки в WebP/AVIF.
- Ресайзит их под размер экрана (не грузит 4K картинку на телефон).
- Резервирует место на странице, чтобы не было скачков.
import Image from 'next/image';
import heroPic from './hero.png';
export default function Hero() {
return (
<Image
src={heroPic}
alt="Hero Image"
placeholder="blur" // Пока грузится, показывает размытую версию
/>
);
}**2. next/font**
Шрифты скачиваются и инлайнятся прямо в CSS на этапе билда. Никаких запросов к Google Fonts во время загрузки страницы и никакого мигания текста.
Это процесс, когда React "оживляет" статический HTML.
- Сервер отдает HTML (пользователь видит кнопку, но нажать нельзя).
- Браузер грузит JS.
- React проходит по HTML и навешивает обработчики событий (
onClick).
Ошибка "Hydration Mismatch": Если HTML, сгенерированный на сервере, отличается от того, что React попытался нарисовать в браузере (например, вы использовали Date.now() или Math.random() при рендере), вы увидите ошибку.
- Server Components (По умолчанию в Next.js):
- Рендерятся только на сервере.
- Их код (JS) никогда не отправляется в браузер.
- Не могут: использовать хуки (
useState,useEffect), слушатели событий (onClick). - Могут: читать БД, файловую систему, хранить секреты (API keys).
- Client Components (Директива
'use client'):
- Это старый добрый React, который мы учили в главах 1-18.
- Рендерятся на сервере (в HTML) + гидратируются в браузере.
- Используются там, где нужна интерактивность (кнопки, формы, анимации).
Аналогия для Java-разработчика:
- Server Component = JSP/Thymeleaf (рендерит статику на бэке).
- Client Component = JavaScript виджет, вставленный внутрь JSP.
- Магия: В React граница между ними бесшовная. Вы импортируете клиентский компонент внутрь серверного как обычный модуль.
Это "Killer Feature". Нам больше не нужно создавать REST API (Controller -> Service -> Repository) только для того, чтобы отдать данные фронтенду. Серверный компонент может сам сходить в базу.
Было (React SPA):
- Browser:
useEffect->fetch('/api/users'). - Server: Controller
/api/users->db.query().
Стало (RSC):
// app/users/page.tsx
// Это Server Component (по дефолту)
import { db } from '@/lib/db';
export default async function UsersPage() {
// Прямой запрос в БД. Этот код останется на сервере!
const users = await db.user.findMany();
return (
<main>
<h1>Список пользователей</h1>
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</main>
);
}Преимущество "Zero Bundle Size":
Если вы используете тяжелую библиотеку (например, moment.js или парсер Markdown) внутри серверного компонента, она выполнится на сервере, а пользователь получит только результат (HTML). Вес JS-бандла для пользователя — 0 байт.
Как отправить форму? Раньше мы писали onSubmit, делали fetch('POST', ...) и обновляли стейт.
Теперь мы используем Server Actions. Это функции, которые выглядят как обычный JS, но выполняются на сервере (RPC — Remote Procedure Call).
// app/actions.ts
'use server'; // Директива: "Эта функция - API эндпоинт"
import { db } from '@/lib/db';
import { revalidatePath } from 'next/cache';
export async function createUser(formData: FormData) {
const name = formData.get('name');
// 1. Мутация БД
await db.user.create({ data: { name } });
// 2. Инвалидация кэша (React обновит UI сам)
revalidatePath('/users');
}Использование в компоненте:
// app/users/add-user.tsx
import { createUser } from '@/app/actions';
export default function AddUserForm() {
// Мы передаем серверную функцию прямо в action формы!
// Работает даже БЕЗ JavaScript в браузере (progressive enhancement).
return (
<form action={createUser}>
<input name="name" type="text" />
<button type="submit">Создать</button>
</form>
);
}Что если запрос в БД долгий (3 секунды)? В обычном SSR пользователь 3 секунды смотрит на белый экран. В RSC мы используем Streaming.
Мы оборачиваем медленную часть в <Suspense>.
- Сервер мгновенно отдает "каркас" (Header, Sidebar).
- Медленная часть заменяется на спиннер (Fallback).
- Сервер держит соединение открытым и "досылает" кусок HTML, когда данные из БД готовы.
import { Suspense } from 'react';
import UserList from './UserList'; // Медленный компонент с await db...
export default function Page() {
return (
<section>
<h1>Главная</h1>
<Suspense fallback={<p>Загрузка списка...</p>}>
{/* Этот компонент начнет грузиться, не блокируя заголовок */}
<UserList />
</Suspense>
</section>
);
}Стандартный подход "Group by Type" (папки components, hooks, utils) работает только для маленьких проектов.
В Enterprise сейчас набирает популярность методология Feature-Sliced Design (FSD).
Суть: Делим приложение не по техническому признаку (компоненты/хуки), а по бизнес-ценности (слайсам).
Слои (Layers) снизу вверх:
- Shared: Переиспользуемый код, не привязанный к бизнесу (UI-kit, API client, хелперы).
- Entities: Бизнес-сущности (User, Product, Order). Содержат модель данных и UI-карточки.
- Features: Действия пользователя (AuthByPhone, AddToCart, LikePost).
- Widgets: Самостоятельные блоки страницы (Header, ProductList).
- Pages: Страницы (композиция виджетов).
- App: Глобальные настройки (Роутинг, Провайдеры, Глобальные стили).
Пример структуры папок:
src/
├── app/ # Инициализация (Providers, Router)
├── pages/ # Страницы
│ └── home/
├── widgets/ # Header, Sidebar, Feed
├── features/ # Use cases
│ ├── auth/ # Фича авторизации
│ │ ├── ui/ # LoginForm.tsx
│ │ ├── model/ # authSlice.ts (Redux)
│ │ └── api/ # authApi.ts
│ └── add-to-cart/
├── entities/ # Сущности
│ ├── user/ # UserAvatar, useUser
│ └── product/ # ProductCard, ProductType
└── shared/ # UI Kit, lib, config
├── ui/ # Button, Input (глупые компоненты)
└── api/ # Axios instance
Правило: Слой выше может использовать слой ниже. Слой ниже НЕ может знать о слое выше.
Пример: Feature может импортировать Entity, но Entity не может импортировать Feature.
Когда у вас есть:
- Админка (React)
- Клиентский сайт (Next.js)
- UI Kit (общий для обоих)
Вместо 3 разных репозиториев (и ада с версионированием UI-кита через npm publish), мы кладем всё в один Monorepo.
Инструменты:
- Turborepo (от Vercel): Очень быстрый, легкий в настройке. Стандарт для экосистемы JS.
- Nx: Мощный, enterprise-ready, поддерживает Angular/React/Node, имеет генераторы кода (близко к Maven modules).
Структура монорепозитория:
my-company/
├── apps/
│ ├── web/ # Next.js app
│ └── admin/ # Vite SPA app
├── packages/
│ ├── ui/ # Общие кнопки, инпуты
│ ├── config/ # Общие настройки ESLint, TS
│ └── utils/ # Общие функции (formatDate)
Архитектурный паттерн, когда разные части страницы (Хедер, Корзина, Основной контент) разрабатываются разными командами, деплоятся независимо и собираются в браузере пользователя.
Технология: Webpack 5 Module Federation.
Сценарий:
- Команда A делает "Checkout".
- Команда B делает "Catalog".
- Команда C делает "Shell" (оболочку).
Команда A может выкатить новый Checkout в продакшн, не пересобирая Catalog и Shell.
Предупреждение архитектора: Микрофронтенды приносят огромную сложность (общие зависимости, версионирование, стили, коммуникация). Используйте их только тогда, когда у вас 3+ независимые Frontend-команды, которые блокируют друг друга при релизах.
В больших проектах компоненты разрабатываются изолированно от бизнес-логики. Storybook — это "песочница" для компонентов.
- Документация: Разработчик видит все варианты кнопки (Primary, Secondary, Disabled) и может потыкать пропсы.
- Изоляция: Вы разрабатываете UI-кит, даже если бэкенд еще не готов.
- Visual Testing: Автоматическое сравнение скриншотов (Chromatic), чтобы убедиться, что CSS-правка не сломала верстку.
Если ваш сайт работает на нескольких языках, хардкодить текст (<h1>Привет</h1>) нельзя.
Библиотеки: react-i18next или react-intl.
Принцип работы:
- Весь текст выносится в JSON-словари.
- В коде используются ключи.
// ru.json
{
"welcome_user": "Привет, {{name}}!",
"cart": {
"total": "Итого: {{amount, currency}}"
}
}
// Component.jsx
import { useTranslation } from 'react-i18next';
const Header = ({ user }) => {
const { t } = useTranslation();
return (
<h1>
{/* "Привет, Dmitry!" */}
{t('welcome_user', { name: user.name })}
</h1>
);
};Форматирование: Используйте нативный браузерный Intl API для дат и валют. Не нужно тащить Moment.js.
const price = 12345.67;
// Автоматически отформатирует как "12 345,67 ₽" для ru-RU
new Intl.NumberFormat('ru-RU', { style: 'currency', currency: 'RUB' }).format(price);Как Java-разработчик, вы знаете, что Android пишется на Kotlin/Java. Но с React Native вы можете писать на JS/TS и получать нативное приложение (не WebView!).
Основные отличия от React Web:
- Нет HTML-тегов:
<div>-><View><span>/<p>-><Text><button>-><TouchableOpacity><img>-><Image>
- Стили: Не CSS, а JS-объекты (Flexbox работает, но Grid — нет).
- Навигация: Не URL, а стековая навигация (Stack Navigator).
Expo: Это фреймворк поверх React Native (аналог Next.js для Web). Он упрощает доступ к камере, геолокации и пуш-уведомлениям. Совет: Если нужно быстро сделать мобильное приложение — берите Expo.
Мы прошли путь от console.log до микрофронтендов.
Roadmap для собеседований (Senior React):
- Core: Event Loop, Closures, Prototype,
this. - React: Reconciliation, Fiber, смысл
key, useEffect dependencies, Rules of Hooks. - State Managers: Redux (архитектура Flux) vs Zustand/MobX.
- Performance: Как профилировать (React DevTools Profiler), useMemo, React.memo, виртуализация.
- System Design: Как спроектировать ленту новостей? Как сделать i18n? Как настроить CI/CD для фронтенда?
Как следить за обновлениями:
- React 19 (вышел недавно):
usehook, компилятор React Compiler (убирает нужду в useMemo), Server Actions. - Блог Дэна Абрамова (overreacted.io) — глубокое понимание.
- Next.js Conf — новые тренды SSR.