React — компоненты-рецепты
Подборка готовых React-компонентов с построчным разбором — «что написано» и «зачем так». Материал для тех, кто ищет «react counter example», «react todo list», «how to make a button in react», «react usestate example», «react form submit preventdefault», «react fetch data useeffect» или сдаёт лабораторную / курсовую по веб-разработке.
Для кого эта статья
| Аудитория | Зачем открыть |
|---|---|
| Школьники | Информатика, кружок, первый сайт с кнопками вместо консоли |
| Студенты | Лабораторная «интерфейс на React», сравнение с Tkinter или Swing |
| Самоучки | Скопировать .jsx → npm run dev → разобрать таблицу под кодом |
| После HTML/CSS | Уже делали макеты — те же формы, но state живёт в JavaScript |
Каждый блок ниже можно скопировать целиком в src/App.jsx (или в отдельный файл в src/components/) и сразу увидеть результат в браузере.
Обзор React — библиотека для пользовательских интерфейсов. Пошаговый старт — Первая программа на React. Справочник API — 271. HTTP-запросы — Fetch / axios и curl / fetch. Стили — HTML + CSS — макеты, Tailwind — готовые блоки. Десктоп на Python — Tkinter — окна и виджеты. Мобильный UI на Dart — Flutter и Flutter — готовые виджеты. Эта статья — практическая галерея, как примеры Turtle для рисования.
Как пользоваться статьёй
- Создайте проект Vite + React — команды в каркасе.
- Скопируйте весь блок кода примера в
src/App.jsx(или вынесите вsrc/components/). - Запустите
npm run dev, откройте адрес из консоли (обычноhttp://localhost:5173). - Прочитайте Разбор и таблицу под кодом — там смысл строк и типичные ошибки.
- Измените текст, цвета, начальные данные — так быстрее запоминается API.
Краткий указатель — что ищут в Google
| Раздел | Типичный запрос |
|---|---|
| Каркас проекта | vite react template, npm create vite react, react hello world |
| Счётчик | react usestate counter, react onclick button, increment counter react |
| Приветствие | react controlled input, react onchange input, react form name |
| Переключатель | react toggle button, react boolean usestate |
| Настройки | react checkbox example, react radio button, controlled checkbox react |
| Todo | react todo list, react map key, todo app react hooks |
| Конвертер | react number input, react calculator example, temperature converter react |
| Модальное окно | react modal component, react dialog popup, conditional render modal |
| Вкладки | react tabs component, active tab state react |
| Поиск по списку | react filter list, react search input filter array |
| Форма входа | react login form, react form validation, react preventdefault submit |
| Загрузка и ошибка | react loading spinner, react conditional render loading |
| Список с API | react fetch useeffect, react get data from api, react useeffect empty array |
| Подъём state | react lifting state up, react props callback parent |
| Свой хук | react custom hook, uselocalstorage react hook |
| Тема Context | react context example, react dark mode context |
| Router | react router v6 example, react routes link, react spa navigation |
| Лендинг | react component composition, react landing page sections |
Словарь React за 30 секунд
| Понятие | Зачем | Как в коде |
|---|---|---|
| Компонент | Кусок UI — функция, возвращающая JSX | function App() { return <div>…</div> } |
| JSX | «HTML внутри JavaScript» | <button onClick={…}>Текст</button> |
| Props | Входные данные от родителя | function Btn({ label, onClick }) |
| State | Данные, меняющиеся со временем | const [n, setN] = useState(0) |
| Setter | Записать новое значение state | setN(n + 1) или setN((x) => x + 1) |
| Controlled input | Поле подчинено state | value={text} + onChange={…} |
| key | ID элемента в списке | {items.map((x) => <li key={x.id}>… |
| useEffect | Побочный эффект после рендера | fetch, таймер, document.title |
| children | Содержимое между тегами компонента | <Modal>…текст…</Modal> |
Односторонний поток данных: state хранит владелец (обычно родитель). Детям передают props и колбэки (onSave, onClick). Менять props внутри ребёнка нельзя — только вызвать переданную функцию.
Как работает React — цикл обновления
Браузер не перезагружает всю страницу при каждом клике. React пересчитывает, что изменилось, и обновляет DOM точечно.
Пользователь нажал «+» → setCount → React снова вызвал App с новым count → на экране обновилась только цифра в заголовке.
Обязательный каркас
Любой пример ниже опирается на проект Vite + React. Запомните команды — как import tkinter и mainloop() в Tkinter.
Задача: поднять пустой React-проект и убедиться, что dev-сервер работает.
npm create vite@latest my-react-app -- --template react
cd my-react-app
npm install
npm run dev
| Команда | Что делает |
|---|---|
npm create vite@latest | Скачивает шаблон проекта (быстрее старого create-react-app) |
npm install | Ставит зависимости из package.json |
npm run dev | Поднимает сервер; при сохранении файла страница обновляется |
Минимальный src/App.jsx:
import './App.css';
export default function App() {
return (
<div className="app">
<h1>Моё React-приложение</h1>
{/* сюда вставляете код из примеров ниже */}
</div>
);
}
Разбор:
| Фрагмент | Смысл |
|---|---|
export default function App | Корневой компонент — точка входа UI; Vite подключает его в main.jsx |
className="app" | CSS-класс. В JSX пишут className, потому что class — зарезервированное слово в JavaScript |
return ( … ) | JSX — результат функции; React рисует это в DOM |
{/* … */} | Комментарий внутри JSX |
Типичные ошибки при старте:
- Забыли
npm install— командаnpm run devпадает с «module not found». - Редактируете не тот файл — рабочий код обычно в
src/App.jsx, не вindex.html. - Порт занят — Vite предложит другой адрес в консоли, откройте его.
Попробуйте: добавьте import { useState } from 'react' и перейдите к счётчику.
Стартовые компоненты
Простые блоки «с нуля» — с них удобно начинать лабораторную или первый commit в GitHub.
Счётчик
Задача: показать, что React хранит число в памяти и обновляет экран по клику — минимум для проверки установки. Классический react counter example.
import { useState } from 'react';
export default function App() {
const [count, setCount] = useState(0);
return (
<div className="app">
<h1>Счётчик: {count}</h1>
<button type="button" onClick={() => setCount(count - 1)}>−</button>
<button type="button" onClick={() => setCount(0)}>Сброс</button>
<button type="button" onClick={() => setCount(count + 1)}>+</button>
</div>
);
}
Разбор:
import { useState } from 'react'— хук для локального state в функциональном компоненте.useState(0)создаёт пару: текущее значениеcountи функциюsetCountдля записи нового.{count}в JSX — вставка JavaScript-выражения в разметку; при измененииcountReact перерисует только этот текст.onClick={() => setCount(count + 1)}— в обработчик передают функцию. Если написатьonClick={setCount(count + 1)}, setter вызовется сразу при рендере — типичная ошибка новичка.type="button"— кнопка внутри<form>случайно не отправит форму.
| Строка | Смысл |
|---|---|
useState(0) | Начальное значение счётчика — ноль |
setCount(0) | Явный сброс |
setCount((c) => c + 1) | Безопаснее при частых кликах — берёт предыдущее значение |
Попробуйте: замените setCount(count + 1) на setCount((c) => c + 1). Добавьте useEffect, меняющий document.title — см. 272.
Поле имени и приветствие
Задача: прочитать текст из поля и показать результат на экране — типичная форма «введите имя». Основа controlled input в React.
import { useState } from 'react';
export default function App() {
const [name, setName] = useState('');
return (
<div className="app">
<label>
Ваше имя:
<input
type="text"
value={name}
placeholder="Анна"
onChange={(e) => setName(e.target.value)}
/>
</label>
{name.trim() && <h2>Привет, {name.trim()}!</h2>}
</div>
);
}
Разбор:
value={name}делает input контролируемым: на экране только то, что лежит в state React.onChange={(e) => setName(e.target.value)}— при каждом символе читаем текст из DOM (e.target.value) и кладём в state.{name.trim() && <h2>…}— условный рендеринг: блок<h2>рисуется, только если послеtrim()имя не пустое.trim()убирает пробелы по краям — иначе один пробел уже покажет «Привет».
| Без state | С state (React) |
|---|---|
Браузер сам хранит текст в <input> | React хранит текст в name, input его отображает |
Читают через ref или DOM | Читают через name в коде — удобно для валидации и отправки |
Попробуйте: кнопка «Очистить» — onClick={() => setName('')}. Поле «Фамилия» — второй useState.
Переключатель вкл/выкл
Задача: boolean state и смена подписи по клику — основа переключателей, «лайков», видимости блоков.
import { useState } from 'react';
export default function App() {
const [on, setOn] = useState(false);
return (
<div className="app">
<p>Статус: {on ? 'включено' : 'выключено'}</p>
<button type="button" onClick={() => setOn((v) => !v)}>
{on ? 'Выключить' : 'Включить'}
</button>
</div>
);
}
Разбор:
useState(false)— начальное значение «выключено».setOn((v) => !v)переключает boolean от предыдущего значения — надёжнее, чемsetOn(!on)при сложной логике.- Тернарный оператор
on ? '…' : '…'в JSX выбирает текст кнопки и статуса.
Попробуйте: добавьте className={on ? 'active' : ''} на контейнер и стиль в App.css.
Флажки и переключатели роли
Задача: несколько настроек «вкл/выкл» и выбор одного варианта из списка (роль, режим, тариф).
import { useState } from 'react';
export default function App() {
const [notify, setNotify] = useState(true);
const [sound, setSound] = useState(false);
const [role, setRole] = useState('user');
const enabled = [
notify && 'уведомления',
sound && 'звук',
].filter(Boolean);
return (
<div className="app">
<label>
<input
type="checkbox"
checked={notify}
onChange={(e) => setNotify(e.target.checked)}
/>
Уведомления
</label>
<label>
<input
type="checkbox"
checked={sound}
onChange={(e) => setSound(e.target.checked)}
/>
Звук
</label>
<p>Роль:</p>
<label>
<input
type="radio"
name="role"
checked={role === 'user'}
onChange={() => setRole('user')}
/>
Пользователь
</label>
<label>
<input
type="radio"
name="role"
value="admin"
checked={role === 'admin'}
onChange={() => setRole('admin')}
/>
Администратор
</label>
<p className="hint">
Роль: {role}; включено: {enabled.length ? enabled.join(', ') : 'ничего'}
</p>
</div>
);
}
Разбор:
checked={notify}+onChangeсe.target.checked— контролируемый checkbox (галочка синхронизирована со state).- У radio одно имя
name="role", разныеvalue; в state хранится выбранная строка'user'или'admin'. .filter(Boolean)убираетfalseиз массива — остаются только включённые опции для строки статуса.
Попробуйте: добавьте третий checkbox «Тёмная тема» и выводите его в enabled.
Компоненты-рецепты
Типовые блоки интерфейса для лабораторных, портфолио и pet-проектов. Каждый можно вынести в src/components/Имя.jsx.
Todo-список с фильтром
Задача: добавление задач, отметка «готово», фильтр «все / активные / выполненные» — react todo list, классика для GitHub-портфолио.
import { useState } from 'react';
const FILTERS = ['all', 'active', 'done'];
export default function App() {
const [text, setText] = useState('');
const [filter, setFilter] = useState('all');
const [items, setItems] = useState([
{ id: 1, title: 'Изучить JSX', done: false },
{ id: 2, title: 'Собрать Todo', done: true },
]);
function addItem(e) {
e.preventDefault();
const title = text.trim();
if (!title) return;
setItems((prev) => [
...prev,
{ id: Date.now(), title, done: false },
]);
setText('');
}
function toggle(id) {
setItems((prev) =>
prev.map((item) =>
item.id === id ? { ...item, done: !item.done } : item
)
);
}
const visible = items.filter((item) => {
if (filter === 'active') return !item.done;
if (filter === 'done') return item.done;
return true;
});
return (
<div className="app">
<h1>Задачи</h1>
<form onSubmit={addItem}>
<input
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Новая задача"
/>
<button type="submit">Добавить</button>
</form>
<div className="filters">
{FILTERS.map((f) => (
<button
key={f}
type="button"
className={filter === f ? 'active' : ''}
onClick={() => setFilter(f)}
>
{f === 'all' ? 'Все' : f === 'active' ? 'Активные' : 'Готово'}
</button>
))}
</div>
<ul>
{visible.map((item) => (
<li key={item.id}>
<label>
<input
type="checkbox"
checked={item.done}
onChange={() => toggle(item.id)}
/>
<span className={item.done ? 'done' : ''}>{item.title}</span>
</label>
</li>
))}
</ul>
{visible.length === 0 && <p>Список пуст для выбранного фильтра</p>}
</div>
);
}
Разбор:
| Строка / конструкция | Смысл |
|---|---|
e.preventDefault() | Форма не перезагружает страницу при Enter или клике «Добавить» |
setItems((prev) => [...prev, newItem]) | Новый массив — React видит изменение. prev.push(x) сломает обновление |
{ ...item, done: !item.done } | Копия объекта с одним изменённым полем — без мутации |
key={item.id} | Стабильный ключ для .map(). React сопоставляет строки между рендерами |
Date.now() как id | Подходит для учебного проекта; в проде — id с сервера |
filterв state и.filter()на массиве — два разных «filter»: первый — режим UI, второй — метод массива.className={item.done ? 'done' : ''}— зачёркивание через CSS (.done { text-decoration: line-through; }).
Попробуйте: кнопка «Удалить выполненные» — setItems(items.filter((x) => !x.done)). Счётчик «Осталось: N» — items.filter((x) => !x.done).length.
Конвертер °C → °F
Задача: ввод числа, формула, вывод результата — частое задание на информатике (аналог конвертера в Tkinter).
import { useState } from 'react';
export default function App() {
const [raw, setRaw] = useState('');
const [error, setError] = useState('');
const [result, setResult] = useState('—');
function convert() {
const normalized = raw.trim().replace(',', '.');
const celsius = Number(normalized);
if (Number.isNaN(celsius)) {
setError('Введите число, например 25');
setResult('—');
return;
}
setError('');
const fahrenheit = celsius * 9 / 5 + 32;
setResult(`${celsius.toFixed(1)} °C = ${fahrenheit.toFixed(1)} °F`);
}
return (
<div className="app">
<h1>Конвертер температуры</h1>
<label>
Температура (°C):
<input
value={raw}
onChange={(e) => setRaw(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && convert()}
/>
</label>
<button type="button" onClick={convert}>Перевести</button>
{error && <p className="error">{error}</p>}
<p>{result}</p>
</div>
);
}
Разбор:
replace(',', '.')— пользователь может ввести25,5с русской раскладкой.Number.isNaN(celsius)— проверка после преобразования; пустая строка дастNaN.- Формула: $F = C \times \frac{9}{5} + 32$.
onKeyDown+Enter— перевод по Enter без отдельной кнопки (удобно на формах).
Попробуйте: кнопка «Очистить» — setRaw(''), setResult('—'), setError('').
Модальное окно
Задача: показать диалог поверх страницы без библиотек — react modal component с нуля.
import { useState } from 'react';
function Modal({ open, title, children, onClose }) {
if (!open) return null;
return (
<div className="modal-backdrop" onClick={onClose}>
<div
className="modal"
role="dialog"
aria-modal="true"
onClick={(e) => e.stopPropagation()}
>
<header>
<h2>{title}</h2>
<button type="button" aria-label="Закрыть" onClick={onClose}>×</button>
</header>
<div className="modal-body">{children}</div>
</div>
</div>
);
}
export default function App() {
const [open, setOpen] = useState(false);
return (
<div className="app">
<button type="button" onClick={() => setOpen(true)}>Открыть окно</button>
<Modal open={open} title="Подтверждение" onClose={() => setOpen(false)}>
<p>Вы уверены, что хотите продолжить?</p>
<button type="button" onClick={() => setOpen(false)}>OK</button>
</Modal>
</div>
);
}
Разбор:
if (!open) return null— компонент вообще не рисуется, когда диалог закрыт (экономия DOM).Modalполучает данные через props:open,title,onClose,children.onClick={onClose}на backdrop — клик по затемнению закрывает окно.e.stopPropagation()на.modal— клик внутри окна не «пробивает» на backdrop.role="dialog"иaria-modal— базовая доступность для скринридеров.
Минимальные стили в App.css:
.modal-backdrop {
position: fixed;
inset: 0;
background: rgb(0 0 0 / 45%);
display: grid;
place-items: center;
z-index: 1000;
}
.modal {
background: #fff;
padding: 1rem 1.25rem;
border-radius: 8px;
min-width: 280px;
box-shadow: 0 8px 32px rgb(0 0 0 / 20%);
}
Попробуйте: второе модальное окно «Ошибка» с другим title и тем же компонентом Modal.
Вкладки
Задача: переключение блоков контента без перезагрузки — аналог вкладок в браузере или настройках приложения.
import { useState } from 'react';
const TABS = [
{ id: 'home', label: 'Главная', body: 'Добро пожаловать на сайт.' },
{ id: 'about', label: 'О нас', body: 'Мы учим React на практике.' },
{ id: 'contact', label: 'Контакты', body: 'hello@example.com' },
];
export default function App() {
const [active, setActive] = useState('home');
const current = TABS.find((t) => t.id === active);
return (
<div className="app">
<div className="tabs" role="tablist">
{TABS.map((tab) => (
<button
key={tab.id}
type="button"
role="tab"
aria-selected={active === tab.id}
className={active === tab.id ? 'active' : ''}
onClick={() => setActive(tab.id)}
>
{tab.label}
</button>
))}
</div>
<div role="tabpanel">{current?.body}</div>
</div>
);
}
Разбор:
- Массив
TABSхранит id, подпись кнопки и текст вкладки — удобно расширять без копипасты JSX. active— id текущей вкладки; один state управляет и кнопками, и содержимым.TABS.find((t) => t.id === active)— объект текущей вкладки для выводаbody.current?.body— optional chaining: если id не найден, ошибки нет.aria-selected— подсказка assistive tech, какая вкладка активна.
Попробуйте: добавьте четвёртую вкладку с JSX вместо строки — например, <ul><li>Пункт</li></ul> в поле body (тогда body станет React-узлом, не строкой).
Поиск по списку
Задача: фильтрация массива по подстроке в реальном времени — основа поиска в таблицах, каталогах, автодополнении.
import { useState } from 'react';
const CITIES = [
'Москва', 'Санкт-Петербург', 'Казань', 'Новосибирск', 'Екатеринбург',
];
export default function App() {
const [query, setQuery] = useState('');
const q = query.trim().toLowerCase();
const filtered = q
? CITIES.filter((city) => city.toLowerCase().includes(q))
: CITIES;
return (
<div className="app">
<input
type="search"
placeholder="Город…"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<ul>
{filtered.map((city) => (
<li key={city}>{city}</li>
))}
</ul>
{filtered.length === 0 && <p>Ничего не найдено</p>}
</div>
);
}
Разбор:
toLowerCase()на обеих сторонах — поиск без учёта регистра (москнайдёт «Москва»)..includes(q)— подстрока где угодно в названии.- Пустой
query— показываем весь списокCITIES. key={city}допустим, потому что названия уникальны; при дубликатах нужен отдельныйid.
Попробуйте: подсветка совпадения через <mark>. Для длинных списков — useMemo (см. справочник).
Форма входа с проверкой
Задача: email, пароль, сообщения об ошибках — типичный экран react login form для учебного проекта.
import { useState } from 'react';
function validate(email, password) {
const errors = {};
if (!email.includes('@')) errors.email = 'Нужен символ @';
if (password.length < 6) errors.password = 'Минимум 6 символов';
return errors;
}
export default function App() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [errors, setErrors] = useState({});
const [ok, setOk] = useState('');
function onSubmit(e) {
e.preventDefault();
const next = validate(email, password);
setErrors(next);
setOk('');
if (Object.keys(next).length === 0) {
setOk(`Вход выполнен для ${email}`);
}
}
return (
<div className="app">
<h1>Вход</h1>
<form onSubmit={onSubmit} noValidate>
<label>
Email
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
{errors.email && <span className="error">{errors.email}</span>}
</label>
<label>
Пароль
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
{errors.password && <span className="error">{errors.password}</span>}
</label>
<button type="submit">Войти</button>
</form>
{ok && <p className="success">{ok}</p>}
</div>
);
}
Разбор:
| Часть | Смысл |
|---|---|
onSubmit={onSubmit} | Отправка формы перехватывается JavaScript |
e.preventDefault() | Страница не перезагружается (иначе state пропадёт) |
noValidate | Отключаем встроенную HTML-валидацию — показываем свои сообщения |
Объект errors | Ключ = поле, значение = текст ошибки; пустой объект = всё OK |
type="password" | Символы скрыты; в учебном примере пароль только в state, без отправки на сервер |
Попробуйте: блокировать кнопку «Войти», пока поля пустые — disabled={!email || !password}.
Загрузка, ошибка, пустой список
Задача: три состояния UI при асинхронной работе — loading, error, data. Шаблон для любого fetch.
import { useEffect, useState } from 'react';
export default function App() {
const [status, setStatus] = useState('loading'); // loading | ok | error
const [data, setData] = useState([]);
useEffect(() => {
const timer = setTimeout(() => {
if (Math.random() > 0.2) {
setData(['Альфа', 'Бета', 'Гамма']);
setStatus('ok');
} else {
setStatus('error');
}
}, 800);
return () => clearTimeout(timer);
}, []);
if (status === 'loading') return <p>Загрузка…</p>;
if (status === 'error') return <p className="error">Не удалось загрузить данные</p>;
if (!data.length) return <p>Список пуст</p>;
return (
<ul>
{data.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
);
}
Разбор:
- Ранний
returnдля каждого состояния — читаемый условный рендеринг без вложенных тернарников. useEffect(..., [])— эффект один раз после первого появления компонента на экране.return () => clearTimeout(timer)— cleanup: если компонент исчезнет до срабатывания таймера, таймер отменится (нет утечки).- В реальном проекте вместо
setTimeout—fetch; логика состояний та же.
Попробуйте: замените таймер на fetch('https://jsonplaceholder.typicode.com/users') и .then((r) => r.json()).
Список заметок с API
Задача: загрузить JSON с сервера и отрисовать список — связка useEffect + fetch, как в 272.
import { useEffect, useState } from 'react';
export default function NotesList() {
const [notes, setNotes] = useState([]);
const [error, setError] = useState('');
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('http://127.0.0.1:3000/notes')
.then((res) => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
})
.then(setNotes)
.catch((e) => setError(e.message))
.finally(() => setLoading(false));
}, []);
if (loading) return <p>Загрузка…</p>;
if (error) return <p className="error">Ошибка: {error}</p>;
if (!notes.length) return <p>Заметок пока нет</p>;
return (
<ul>
{notes.map((n) => (
<li key={n.id}>{n.text}</li>
))}
</ul>
);
}
Разбор:
| Строка | Смысл |
|---|---|
useState([]) | Пока данных нет — пустой массив |
useEffect(..., []) | Запрос один раз при монтировании; без [] — бесконечные запросы |
if (!res.ok) | HTTP 404/500 — ошибка до парсинга JSON |
.then(setNotes) | setNotes получит готовый массив из .json() |
.finally(() => setLoading(false)) | Спиннер скрывается и при успехе, и при ошибке |
key={n.id} | Стабильный ключ из данных API |
Подключите в App.jsx: <NotesList /> под другими блоками.
CORS и прокси Vite — 264. Node API — 262. Готовые шаблоны fetch — 1145.
Попробуйте: POST-заметку через fetch с method: 'POST' и JSON.stringify({ text }).
Подъём state — переиспользуемый Counter
Задача: вынести кнопки в отдельный компонент, а число хранить в родителе — паттерн lifting state up.
src/components/Counter.jsx:
export function Counter({ value, onIncrement, onDecrement, onReset }) {
return (
<section className="counter">
<h2>Счётчик: {value}</h2>
<button type="button" onClick={onDecrement}>−</button>
<button type="button" onClick={onReset}>Сброс</button>
<button type="button" onClick={onIncrement}>+</button>
</section>
);
}
src/App.jsx:
import { useState } from 'react';
import { Counter } from './components/Counter';
export default function App() {
const [count, setCount] = useState(0);
return (
<Counter
value={count}
onIncrement={() => setCount((c) => c + 1)}
onDecrement={() => setCount((c) => c - 1)}
onReset={() => setCount(0)}
/>
);
}
Разбор:
Counterне вызываетuseState— только отображает props и вызывает колбэки.- Родитель владеет
count— один источник правды для всего дерева. value={count}— данные вниз (props).onIncrement={() => …}— события вверх (колбэки).- Так один state можно разделить между несколькими детьми (например, счётчик и график).
Попробуйте: второй Counter с тем же count — оба синхронизированы автоматически.
Свой хук useLocalStorage
Задача: сохранить значение между перезагрузками вкладки (F5) — типичный custom hook.
src/hooks/useLocalStorage.js:
import { useEffect, useState } from 'react';
export function useLocalStorage(key, initial) {
const [value, setValue] = useState(() => {
try {
const raw = localStorage.getItem(key);
return raw !== null ? JSON.parse(raw) : initial;
} catch {
return initial;
}
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
Использование в App.jsx:
import { useLocalStorage } from './hooks/useLocalStorage';
export default function App() {
const [name, setName] = useLocalStorage('username', '');
return (
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Имя сохранится после F5"
/>
);
}
Разбор:
- Имя хука обязательно начинается с
use— правило React для хуков. useState(() => …)— ленивая инициализация: чтениеlocalStorageодин раз при первом рендере.useEffectзаписывает в storage при каждом измененииvalue.JSON.stringify/parse— работает со строками и числами; для сложных объектов следите за форматом.try/catch— если в storage мусор, вернётсяinitial.
Попробуйте: сохранить тему 'light' | 'dark' тем же хуком.
Тёмная тема через Context
Задача: передать настройку темы глубоко в дерево без «проброса» props через каждый уровень.
src/theme/ThemeContext.jsx:
import { createContext, useContext, useMemo, useState } from 'react';
const ThemeContext = createContext(null);
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const value = useMemo(
() => ({
theme,
toggle: () => setTheme((t) => (t === 'light' ? 'dark' : 'light')),
}),
[theme]
);
return (
<ThemeContext.Provider value={value}>
<div className={`app theme-${theme}`}>{children}</div>
</ThemeContext.Provider>
);
}
export function useTheme() {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error('useTheme вызывают вне ThemeProvider');
return ctx;
}
src/App.jsx:
import { ThemeProvider, useTheme } from './theme/ThemeContext';
function Toolbar() {
const { theme, toggle } = useTheme();
return (
<button type="button" onClick={toggle}>
Тема: {theme}
</button>
);
}
export default function App() {
return (
<ThemeProvider>
<Toolbar />
<main>Контент страницы</main>
</ThemeProvider>
);
}
Разбор:
createContext— «канал» для данных.Provider value={…}— все потомки могут прочитать value черезuseContext.useMemoдля объектаvalue— меньше лишних перерисовок дочерних компонентов.- шаблон
classNameс префиксомtheme-— переключение CSS-класса на корне (класс.theme-darkзадаёт фон и цвет текста).
Попробуйте: обернуть только часть страницы — увидите, что useTheme снаружи Provider выбросит ошибку.
React Router — минимальная навигация
Задача: несколько «страниц» в одностраничном приложении (SPA) без полной перезагрузки документа.
npm install react-router-dom
import { BrowserRouter, Link, NavLink, Route, Routes } from 'react-router-dom';
function Home() {
return <h1>Главная</h1>;
}
function About() {
return <h1>О проекте</h1>;
}
function NotFound() {
return <h1>404 — страница не найдена</h1>;
}
export default function App() {
return (
<BrowserRouter>
<nav>
<Link to="/">Главная</Link>
{' · '}
<NavLink to="/about">О нас</NavLink>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
);
}
Разбор:
| Элемент | Назначение |
|---|---|
BrowserRouter | Связывает URL в адресной строке с деревом React |
Link to="/about" | Переход без перезагрузки HTML-документа |
NavLink | Как Link, плюс класс active для текущего маршрута |
Routes / Route | Какой компонент показать при каком path |
path="*" | Fallback — страница 404 для неизвестных адресов |
Подробнее — 272 — Router. Для SEO и серверного HTML — Next.js.
Попробуйте: маршрут /users/:id и useParams() в компоненте профиля.
Лендинг из секций
Задача: показать архитектуру страницы — каждый блок в своём файле, App только собирает порядок.
import { Header } from './components/layout/Header';
import { Footer } from './components/layout/Footer';
import { Hero } from './components/sections/Hero';
import { Features } from './components/sections/Features';
import { Faq } from './components/sections/Faq';
export default function App() {
return (
<>
<Header />
<main>
<Hero />
<Features />
<Faq />
</main>
<Footer />
</>
);
}
Разбор:
- Фрагмент
<>…</>группирует несколько корневых узлов без лишнего<div>. App— компоновщик: задаёт layout и порядок секций.- Каждая секция — отдельный файл → параллельная работа в команде, проще искать код.
- HTML-разметку секций можно взять из HTML + CSS — макеты или Tailwind — готовые блоки и перенести в JSX (заменить
classнаclassName, закрыть все теги).
Попробуйте: вынесите Hero в отдельный файл с props title и subtitle.
Переиспользуемые базы
Шаблон компонента с props по умолчанию
export function Card({ title = 'Без названия', children }) {
return (
<article className="card">
<h3>{title}</h3>
<div>{children}</div>
</article>
);
}
Разбор: title = '…' в деструктуризации — значение по умолчанию, если prop не передали. children — всё между <Card>…</Card>.
Сброс формы
function resetForm(setEmail, setPassword, setErrors) {
setEmail('');
setPassword('');
setErrors({});
}
Вызывайте после успешной отправки или по кнопке «Очистить».
Частые ошибки
| Симптом | Причина | Что сделать |
|---|---|---|
Too many re-renders | setCount(1) или setCount(count+1) в теле компонента | Setter только в обработчике или useEffect |
| Кнопка срабатывает при загрузке | onClick={handle()} со скобками | onClick={handle} или onClick={() => handle()} |
| Input «не печатает» | Есть value, нет onChange | Добавить controlled-пару |
| Список «ломается» при удалении | key={index} | Стабильный id из данных |
Бесконечный useEffect | Объект/массив в deps создаётся заново каждый рендер | Уточнить deps или useMemo |
fetch failed / CORS | API выключен или нет заголовков CORS | 264, прокси в Vite |
| State «не обновился» | Мутация: arr.push(x), obj.field = 1 | Новая ссылка: [...arr, x], { ...obj, field: 1 } |
| Белый экран после правки | Синтаксическая ошибка JSX | Смотрите красный текст в терминале npm run dev |
Полный список — справочник React.
Что попробовать дальше
| Уровень | Задание | Материал |
|---|---|---|
| Начальный | Todo с удалением и фильтром «активные» | блок Todo |
| Начальный | Заметки с POST и DELETE | 272 — задание «Заметки» |
| Начальный | Калькулятор на несколько кнопок | счётчик + несколько useState |
| Средний | Погода или поиск фильмов по API | useEffect + 1145 |
| Средний | Десктоп на Electron | 118 |
| Средний | TypeScript + типы props | 30 — TypeScript и React |
| Экзамен | 200 вопросов для самопроверки | 133 |
Связанные материалы
- React — обзор — карта тем и Virtual DOM
- Первая программа на React — Vite, хуки, Router, задание «Заметки»
- Справочник по React — API, HOC, тесты
- Fetch / axios — типовые запросы — шаблоны HTTP рядом с React
- Vue и Svelte — готовые компоненты — те же задачи на Vue/Svelte
- 200 вопросов по React — самопроверка перед зачётом
- Tkinter — окна и виджеты — те же задачи на Python
- Java Swing — окна и кнопки — те же задачи на Java
- HTML + CSS — готовые макеты — вёрстка секций для лендинга
- Tailwind — готовые блоки — hero, pricing, navbar на utility-классах
См. также
Другие статьи этого же раздела в боковом меню (как на странице "О разделе"). Практическая карта типовых IT-задач: термины, пошаговое внедрение, проверка качества и типичные ошибки. Простой консольный чат на C# — учебное приложение с сокетами: TCP между клиентом и сервером, многопоточность и обмен сообщениями в консоли. Примеры вёрстки на HTML и CSS с разбором: центрирование, Flexbox, Grid, формы, шапка, подвал и адаптив для учебы и портфолио. Перед началом работы обязательно изучите главу Turtle . Галерея 3D-фигур на Panda3D — карточки, куб, пирамида, сфера, сетки и составные сцены; код для локального запуска. Готовые docker-compose.yml с разбором каждой строки — nginx, PostgreSQL, Redis, WordPress, MongoDB. Примеры для школьников и студентов: postgres example, поднять базу локально, app + db. Примеры nginx.conf для статики, reverse proxy, React/Vue SPA, PHP, SSL и балансировки — построчный разбор директив, проверка curl и типичные ошибки для лабораторных и VPS. dockerfile example — 10 готовых Dockerfile с построчным разбором: node, python, golang, react nginx, spring boot, php, dotnet. Для студентов, лабораторных и docker build с нуля. PromQL example — готовые запросы Prometheus и Grafana с построчным разбором: up, rate, node_exporter cpu, memory, disk, http_requests_total, histogram_quantile p99, алерты. Для студентов, лабораторных и devops docker compose. Готовые манифесты Kubernetes с разбором каждой строки — Pod, Deployment, Service, ConfigMap, Secret, Ingress. Примеры для Minikube, kind и kubectl apply. Примеры графиков Matplotlib на Python для школьников и студентов — sin, cos, парабола, столбцы, scatter, гистограмма, подграфики; код с подробным разбором. Примеры pandas на Python для школьников и студентов — DataFrame, фильтрация, groupby, очистка, merge, сводные таблицы и экспорт; код с подробным разбором каждой строки.Готовые решения
Простой консольный чат на CSharp
HTML + CSS — готовые макеты
Примеры фигур Turtle на Python
Примеры фигур Panda3D на Python
Docker Compose — готовые стеки
Nginx — конфиги под задачу
Dockerfile — 10 типовых образов
Prometheus + Grafana — запросы
Kubernetes YAML — минимальные манифесты
Matplotlib — графики
Pandas — типовые операции