JS_Core_React_Core
React Core
React — библиотека JavaScript для создания пользовательских интерфейсов из компонентов. Компонент получает входные данные, может хранить состояние и возвращает описание интерфейса.
function Greeting() {
return <h1>Привет, React!</h1>;
}
export default function App() {
return <Greeting />;
}Компоненты называют с заглавной буквы. Имя со строчной буквы React воспринимает как встроенный HTML-элемент.
JSX
JSX — расширение синтаксиса JavaScript, позволяющее описывать интерфейс разметкой, похожей на HTML. Сборщик преобразует JSX в обычный JavaScript.
const name = 'Анна';
const element = <h1>Привет, {name}!</h1>;В фигурных скобках размещаются JavaScript-выражения:
<p>{user.name}</p>
<p>{isOnline ? 'В сети' : 'Не в сети'}</p>
<p>Товаров: {items.length}</p>Инструкции вроде if нельзя вставлять непосредственно в {}. Условие вычисляют заранее либо используют тернарный оператор.
Один корневой элемент
Компонент возвращает один корневой элемент. Если лишний HTML-контейнер не нужен, используется фрагмент:
function UserInfo() {
return (
<>
<h2>Анна</h2>
<p>Frontend-разработчик</p>
</>
);
}Отличия от HTML
| HTML | JSX |
|---|---|
class |
className |
for |
htmlFor |
tabindex |
tabIndex |
onclick |
onClick |
Все теги закрываются:
<img src={avatarUrl} alt="Аватар" />
<input type="text" />Строковые атрибуты указываются в кавычках, остальные значения — в фигурных скобках:
<button type="button" disabled={isSaving}>
Сохранить
</button>Инлайн-стили передаются объектом, а свойства пишутся в camelCase:
<p style={{ color: 'blue', fontSize: '18px' }}>Текст</p>React экранирует строки, вставленные через JSX. Необработанный пользовательский HTML нельзя передавать в dangerouslySetInnerHTML: это может привести к XSS.
Функциональные компоненты
Функциональный компонент — JavaScript-функция, возвращающая JSX:
function Button() {
return <button type="button">Нажать</button>;
}
const Logo = () => <strong>My App</strong>;Интерфейс разбивают на небольшие компоненты:
function Header() {
return <header>Панель управления</header>;
}
function Dashboard() {
return (
<div>
<Header />
<main>Основное содержимое</main>
</div>
);
}Рендеринг должен оставаться чистым: в теле компонента не следует выполнять запросы, менять DOM или модифицировать внешние переменные. Побочные эффекты выполняются в обработчиках событий или useEffect.
Props
Props — входные данные, которые родитель передаёт дочернему компоненту:
function UserCard({ name, role = 'Пользователь' }) {
return (
<article>
<h2>{name}</h2>
<p>{role}</p>
</article>
);
}
function App() {
return <UserCard name="Анна" role="Редактор" />;
}Props могут содержать строки, числа, логические значения, массивы, объекты, функции и React-элементы. Компонент не должен изменять полученные props.
children
Содержимое между тегами компонента доступно через prop children:
function Panel({ title, children }) {
return (
<section>
<h2>{title}</h2>
<div>{children}</div>
</section>
);
}
<Panel title="Профиль">
<p>Имя: Анна</p>
</Panel>State и useState
State — внутренние изменяемые данные компонента. Обновление состояния запускает повторный рендеринг.
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<button type="button" onClick={() => setCount(count + 1)}>
Нажатий: {count}
</button>
);
}useState(initialValue) возвращает текущее значение и setter-функцию.
| Характеристика | Props | State |
|---|---|---|
| Источник | Родитель | Сам компонент |
| Изменение | Только родителем | Через setter |
| Назначение | Входные данные | Изменяемые данные UI |
Функциональное обновление
Если новое значение зависит от предыдущего, используется функция:
setCount((previousCount) => previousCount + 1);Это корректно работает и при нескольких обновлениях:
setCount((value) => value + 1);
setCount((value) => value + 1);
setCount((value) => value + 1);Состояние конкретного рендера является снимком. Поэтому сразу после setCount() переменная count всё ещё содержит значение текущего рендера.
Объекты и массивы
State считается иммутабельным. Вместо изменения существующего значения создаётся новое:
const [user, setUser] = useState({ name: 'Анна', city: 'Москва' });
setUser((currentUser) => ({
...currentUser,
city: 'Казань',
}));Операции с массивом:
// Добавление
setTasks((tasks) => [...tasks, newTask]);
// Удаление
setTasks((tasks) => tasks.filter((task) => task.id !== taskId));
// Изменение
setTasks((tasks) =>
tasks.map((task) =>
task.id === taskId
? { ...task, completed: !task.completed }
: task
)
);Ленивая инициализация
Для дорогого начального вычисления в useState передаётся функция:
const [settings, setSettings] = useState(() => {
const saved = localStorage.getItem('settings');
return saved ? JSON.parse(saved) : { theme: 'light' };
});Производные данные
Данные, полностью вычисляемые из props или state, обычно не нужно хранить отдельно:
const total = items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);Иначе связанные значения могут рассинхронизироваться.
Подъём состояния
Если значение нужно нескольким компонентам, состояние поднимают к ближайшему общему родителю. Родитель хранит значение, а дочерние компоненты получают его и обработчики через props.
Правила хуков
Хуки вызываются:
- только на верхнем уровне компонента или пользовательского хука;
- не внутри условий, циклов и вложенных функций;
- только из React-компонентов и пользовательских хуков.
function Profile() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
// ...
}React связывает состояние с порядком вызовов хуков, поэтому порядок должен оставаться одинаковым при каждом рендере.
useEffect
useEffect синхронизирует компонент с внешними системами: браузерными API, сетью, таймерами, подписками и сторонними библиотеками.
import { useEffect } from 'react';
function PageTitle({ title }) {
useEffect(() => {
document.title = title;
}, [title]);
return <h1>{title}</h1>;
}Общая форма:
useEffect(() => {
// Настройка эффекта.
return () => {
// Очистка эффекта.
};
}, [dependencies]);Зависимости
Без массива эффект запускается после каждого завершённого рендера:
useEffect(() => {
console.log('После каждого рендера');
});С пустым массивом эффект настраивается при появлении компонента:
useEffect(() => {
console.log('Подключение');
return () => console.log('Отключение');
}, []);С зависимостями эффект повторно синхронизируется при их изменении:
useEffect(() => {
document.title = `Поиск: ${query}`;
}, [query]);Все реактивные значения, прочитанные внутри эффекта, указываются в зависимостях. Это проверяет правило линтера react-hooks/exhaustive-deps.
Очистка
Очистка отменяет таймеры и подписки:
useEffect(() => {
const id = setInterval(() => {
setNow(new Date());
}, 1000);
return () => clearInterval(id);
}, []);Пример с обработчиком окна:
useEffect(() => {
function handleResize() {
setWidth(window.innerWidth);
}
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);Очистка выполняется перед повторным запуском изменившегося эффекта и при удалении компонента.
Загрузка данных
useEffect(() => {
const controller = new AbortController();
async function loadUser() {
try {
setLoading(true);
setError(null);
const response = await fetch(`/api/users/${userId}`, {
signal: controller.signal,
});
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
setUser(await response.json());
} catch (error) {
if (error.name !== 'AbortError') setError(error);
} finally {
if (!controller.signal.aborted) setLoading(false);
}
}
loadUser();
return () => controller.abort();
}, [userId]);Callback самого useEffect не делают async: он должен вернуть функцию очистки или undefined, тогда как async-функция возвращает Promise.
Когда эффект не нужен
Обычные вычисления выполняются при рендеринге:
const fullName = `${firstName} ${lastName}`;Логику, вызванную конкретным действием пользователя, помещают в обработчик события, а не в эффект.
В режиме разработки StrictMode может выполнить дополнительный цикл настройки и очистки эффекта. Корректно написанная очистка делает такое поведение безопасным.
Условный рендеринг
Для выбора JSX используются обычные конструкции JavaScript.
if
function Status({ isLoggedIn }) {
if (!isLoggedIn) return <LoginForm />;
return <Dashboard />;
}Тернарный оператор
<p>{isAdmin ? 'Администратор' : 'Пользователь'}</p>Логическое &&
{count > 0 && <strong>Уведомлений: {count}</strong>}Выражение 0 && <Component /> отобразит 0, поэтому число лучше сравнивать явно.
Чтобы ничего не отображать, компонент может вернуть null.
Списки и ключи
Список формируется методом map():
function ProductList({ products }) {
return (
<ul>
{products.map((product) => (
<li key={product.id}>
{product.name} — {product.price} ₽
</li>
))}
</ul>
);
}key помогает React сопоставлять элементы между рендерами. Ключ должен быть уникальным среди соседей и стабильным.
<TaskItem key={task.id} task={task} />Не следует создавать ключ через Math.random() или crypto.randomUUID() во время рендера. Индекс массива подходит только для статического списка, порядок и состав которого не меняются.
key не передаётся компоненту как prop. Если идентификатор нужен внутри, его передают отдельно:
<TaskItem key={task.id} taskId={task.id} task={task} />Перед сортировкой props-массива нужно создать копию, поскольку sort() мутирует массив:
const sorted = [...products].sort((a, b) => a.price - b.price);События в React
Обработчик передаётся как функция:
function Button() {
function handleClick() {
console.log('Кнопка нажата');
}
return <button onClick={handleClick}>Нажать</button>;
}Не нужно вызывать функцию во время рендера:
<button onClick={handleClick}>Верно</button>
// <button onClick={handleClick()}>Неверно</button>Для передачи аргумента используется обёртка:
<button onClick={() => onDelete(product.id)}>Удалить</button>React передаёт объект события:
function handleChange(event) {
console.log(event.target.value);
}Часто применяются:
| Свойство или метод | Назначение |
|---|---|
event.target |
Элемент, где возникло событие |
event.currentTarget |
Элемент с обработчиком |
event.preventDefault() |
Отмена стандартного действия |
event.stopPropagation() |
Остановка распространения |
Отправка формы:
function handleSubmit(event) {
event.preventDefault();
saveData();
}
<form onSubmit={handleSubmit}>...</form>Возврат false не отменяет действие браузера: нужен preventDefault().
Обработчики часто передаются дочерним компонентам через props:
function SaveButton({ onSave }) {
return <button onClick={onSave}>Сохранить</button>;
}Формы и controlled inputs
В controlled input текущее значение хранится в state:
function NameForm() {
const [name, setName] = useState('');
function handleSubmit(event) {
event.preventDefault();
console.log({ name });
}
return (
<form onSubmit={handleSubmit}>
<label htmlFor="name">Имя</label>
<input
id="name"
value={name}
onChange={(event) => setName(event.target.value)}
/>
<button type="submit">Отправить</button>
</form>
);
}Связка value и onChange делает React источником истины. Поле с value, но без onChange, доступно только для чтения.
Несколько полей
const [form, setForm] = useState({
name: '',
email: '',
subscribed: false,
});
function handleChange(event) {
const { name, type, value, checked } = event.target;
setForm((currentForm) => ({
...currentForm,
[name]: type === 'checkbox' ? checked : value,
}));
}<input name="name" value={form.name} onChange={handleChange} />
<input name="email" value={form.email} onChange={handleChange} />
<input
name="subscribed"
type="checkbox"
checked={form.subscribed}
onChange={handleChange}
/>Для checkbox используется checked, для textarea и select — value. Значение input type="number" также поступает строкой; преобразование выполняют отдельно через Number().
Простая проверка перед отправкой:
function handleSubmit(event) {
event.preventDefault();
const email = form.email.trim();
if (!email || !email.includes('@')) {
setError('Введите корректный email');
return;
}
setError('');
sendForm({ ...form, email });
}Клиентская валидация не заменяет серверную. Сервер должен повторно проверять входные данные.
Файловое поле обычно остаётся неуправляемым:
function handleSubmit(event) {
event.preventDefault();
const file = event.currentTarget.elements.avatar.files[0];
}
<form onSubmit={handleSubmit}>
<input name="avatar" type="file" />
</form>Жизненный цикл через хуки
Условно выделяют три этапа:
- Mount — компонент добавлен в интерфейс.
- Update — props или state изменились.
- Unmount — компонент удалён.
// Настройка после появления компонента.
useEffect(() => {
const connection = connect();
// Очистка перед удалением.
return () => connection.disconnect();
}, []);Эффект с зависимостью повторно синхронизируется при её изменении:
useEffect(() => {
const connection = connectToRoom(roomId);
return () => connection.disconnect();
}, [roomId]);Перед новой настройкой React вызывает очистку предыдущего эффекта.
Упрощённая последовательность обновления:
- React вызывает компонент и вычисляет JSX — render.
- React применяет изменения к DOM — commit.
- Браузер отображает результат.
- React запускает
useEffect.
Тело компонента может вызываться чаще, чем меняется DOM, поэтому рендер должен быть чистым.
Общий пример: список задач
import { useEffect, useState } from 'react';
function TaskForm({ onAdd }) {
const [title, setTitle] = useState('');
function handleSubmit(event) {
event.preventDefault();
const normalized = title.trim();
if (!normalized) return;
onAdd(normalized);
setTitle('');
}
return (
<form onSubmit={handleSubmit}>
<input
value={title}
onChange={(event) => setTitle(event.target.value)}
placeholder="Новая задача"
/>
<button type="submit">Добавить</button>
</form>
);
}
function TaskList({ tasks, onToggle, onDelete }) {
if (tasks.length === 0) return <p>Задач нет</p>;
return (
<ul>
{tasks.map((task) => (
<li key={task.id}>
<label>
<input
type="checkbox"
checked={task.completed}
onChange={() => onToggle(task.id)}
/>
{task.title}
</label>
<button onClick={() => onDelete(task.id)}>Удалить</button>
</li>
))}
</ul>
);
}
export default function App() {
const [tasks, setTasks] = useState(() => {
const saved = localStorage.getItem('tasks');
return saved ? JSON.parse(saved) : [];
});
useEffect(() => {
localStorage.setItem('tasks', JSON.stringify(tasks));
}, [tasks]);
function addTask(title) {
setTasks((current) => [
...current,
{ id: crypto.randomUUID(), title, completed: false },
]);
}
function toggleTask(id) {
setTasks((current) =>
current.map((task) =>
task.id === id
? { ...task, completed: !task.completed }
: task
)
);
}
function deleteTask(id) {
setTasks((current) => current.filter((task) => task.id !== id));
}
return (
<main>
<h1>Задачи</h1>
<TaskForm onAdd={addTask} />
<TaskList
tasks={tasks}
onToggle={toggleTask}
onDelete={deleteTask}
/>
</main>
);
}Частые ошибки
Прямая мутация state
// Плохо:
tasks.push(newTask);
setTasks(tasks);
// Хорошо:
setTasks((tasks) => [...tasks, newTask]);Вызов обработчика при рендере
// Плохо: onClick={deleteTask(task.id)}
// Хорошо:
onClick={() => deleteTask(task.id)}Нестабильные ключи
// Плохо: key={Math.random()}
// Хорошо: key={task.id}Бесконечный эффект
useEffect(() => {
setCount(count + 1);
}, [count]);Эффект изменяет свою зависимость и запускается снова. Обычно следует пересмотреть необходимость эффекта.
Лишнее состояние
Не храните отдельно отфильтрованный список или итоговую сумму, если их можно вычислить из существующих props и state.
Отсутствие очистки
Таймеры, подписки, обработчики и незавершённые запросы должны освобождаться функцией очистки.
Краткая памятка
- Компонент — функция, возвращающая JSX.
- Props доступны только для чтения.
- State обновляется через setter из
useState. - Объекты и массивы в state обновляются иммутабельно.
- Хуки вызываются только на верхнем уровне.
useEffectнужен для синхронизации с внешними системами.- Эффекты очищают таймеры, подписки и запросы.
- Спискам нужны стабильные ключи из данных.
- Обработчик передаётся как функция, а не вызывается при рендере.
- Controlled input использует
valueилиcheckedвместе сonChange. - Производные значения обычно вычисляются при рендеринге, а не хранятся в state.