Первая программа на React
Первая программа на React
Где применяют React
React — библиотека для пользовательского интерфейса. Вы описываете экран компонентами (функциями, которые возвращают разметку). Когда меняется состояние (число в счётчике, текст в поле), React пересчитывает, что показать, и обновляет DOM точечно — без полной перезагрузки страницы.
Маршруты вроде /about и HTTP-сервер подключают отдельно — React Router или Next.js. Сам React отвечает за экран: один компонент, один URL через Vite, через минуты виден результат.
В этой статье соберём:
- Счётчик и поле имени —
useState - Заголовок вкладки от счётчика —
useEffect - Список заметок с Node API —
fetch
Сравнение: Vue · Angular · Next.js. Склейка с API: 264. Обзор: 27.md. Галерея готовых компонентов с разбором — React — компоненты-рецепты. Аналог для Vue и Svelte — галерея компонентов.
Что получится
| Часть | Результат |
|---|---|
| Проект | Vite + React на http://localhost:5173 |
| UI | Счётчик, ввод имени, список с сервера |
| API | GET http://127.0.0.1:3000/notes (если Node запущен) |
Создание проекта
create-react-app устарел для новых учебных проектов. Стандарт — Vite: быстрый dev-сервер и сборка.
npm create vite@latest my-first-app -- --template react
cd my-first-app
npm install
npm run dev
| Команда | Что делает |
|---|---|
npm create vite@latest | Скачивает шаблон проекта |
npm install | Ставит зависимости из package.json |
npm run dev | Поднимает сервер; при сохранении файла страница обновляется |
В браузере откройте адрес из консоли (обычно http://localhost:5173).
Компонент и useState
src/App.jsx — корневой компонент приложения:
import { useState } from 'react';
import './App.css';
export default function App() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
return (
<div className="app">
<h1>Моя первая программа на React</h1>
<section className="greeting">
<input
type="text"
placeholder="Введите имя"
value={name}
onChange={(e) => setName(e.target.value)}
/>
{name && <h2>Привет, {name}!</h2>}
</section>
<section className="counter">
<h2>Счётчик: {count}</h2>
<div className="buttons">
<button type="button" onClick={() => setCount(count - 1)}>−</button>
<button type="button" onClick={() => setCount(0)}>Сброс</button>
<button type="button" onClick={() => setCount(count + 1)}>+</button>
</div>
</section>
</div>
);
}
Разбор:
useStateподключается изreactи создаёт локальное состояние компонента.const [count, setCount]иconst [name, setName]через деструктуризацию получают текущее значение и setter.value={name}+onChange=...делают поле ввода контролируемым компонентом.- Условие
{name && <h2>...рендерит приветствие только при непустом значении. - Обработчики
onClickпередают функции, поэтому обновление происходит по событию, а не во время рендера.
Разбор по строкам
| Код | Смысл |
|---|---|
useState(0) | React создаёт ячейку памяти: значение 0 и функцию обновления |
const [count, setCount] | Деструктуризация: count — текущее, setCount — "записать новое" |
setCount(count + 1) | После вызова React заново вызывает App с новым count |
value={name} | Input подчинён state: отображается то, что в React |
onChange={(e) => setName(e.target.value)} | При вводе читаем текст из DOM и кладём в state |
{name && <h2>…} | Если name пустой — блок не рисуется |
onClick={() => setCount(count + 1)} | В onClick передаём функцию. onClick={setCount(1)} вызовет её сразу при рендере — ошибка |
JSX — синтаксис "разметка внутри JavaScript". Vite (Babel) превращает <h1> в вызов React.createElement('h1', …).
Однонаправленный поток данных: state живёт в родителе; детям передают props и колбэки для событий.
useEffect — побочные эффекты
Рендер компонента должен быть предсказуемым: только "по state/props нарисовать UI". Всё, что трогает внешний мир — заголовок вкладки, fetch, таймеры — выносят в useEffect:
import { useState, useEffect } from 'react';
export default function App() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `Счётчик: ${count}`;
}, [count]);
// ...
}
| Часть | Смысл |
|---|---|
| Первый аргумент | Функция-эффект — что выполнить |
Второй [count] | Зависимости: повторить эффект, когда изменился count |
[] | Пустой массив — эффект один раз после первого монтирования |
return () => { … } | Cleanup — перед размонтированием или перед следующим запуском |
Пример с таймером:
useEffect(() => {
const id = setInterval(() => setCount((c) => c + 1), 1000);
return () => clearInterval(id);
}, []);
Разбор:
setIntervalзапускает периодическое действие раз в 1000 мс.- Функциональная форма
setCount((c) => c + 1)использует актуальное предыдущее значение. return () => clearInterval(id)очищает таймер при размонтировании и предотвращает утечки.- Пустой массив зависимостей
[]означает запуск эффекта один раз после первого рендера.
setCount((c) => c + 1) — обновление от предыдущего значения.
Загрузка данных — fetch и Node API
Поднимите API заметок на порту 3000. В React (с CORS на сервере — 263.md):
import { useState, useEffect } from 'react';
function NotesList() {
const [notes, setNotes] = useState([]);
const [error, setError] = useState('');
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));
}, []);
if (error) return <p>Ошибка: {error}</p>;
if (!notes.length) return <p>Заметок пока нет</p>;
return (
<ul>
{notes.map((n) => (
<li key={n.id}>{n.text}</li>
))}
</ul>
);
}
Разбор:
notesхранит список,errorхранит сообщение ошибки для UI.useEffect(..., [])запускает загрузку данных только при первом монтировании компонента.- В
.then((res) => ...)проверяетсяres.ok, чтобы корректно обработать HTTP-статусы. .then(setNotes)передаёт в state готовый массив заметок из JSON.notes.map(...)рендерит элементы списка, аkey={n.id}помогает React корректно сопоставлять узлы.
| Строка | Смысл |
|---|---|
useState([]) | Начальный список пустой |
useEffect(..., []) | Загрузить один раз при появлении компонента |
.then(setNotes) | setNotes получит распарсенный массив |
key={n.id} | Стабильный ключ для списка между рендерами |
Галерея типовых запросов с построчным разбором — GET, POST, Bearer, таймаут и React useEffect: Fetch / axios — типовые запросы. Проверка API в терминале — curl / fetch — примеры.
Подключите в App.jsx: <NotesList /> под счётчиком.
Условный рендеринг и списки
Интерфейс почти всегда зависит от данных: показать загрузку, пустой список или карточки.
| Приём | Пример | Когда |
|---|---|---|
&& | {isLoading && <Spinner />} | Показать блок, если условие истинно |
| Тернарный | {error ? <p>{error}</p> : <List />} | Два варианта разметки |
.map() | {items.map((x) => <Row key={x.id} … />)} | Список однотипных элементов |
key — стабильный идентификатор элемента (id из API, лучше не индекс массива при удалении и сортировке). React по key сопоставляет старые и новые узлы и сохраняет фокус и внутреннее состояние строки.
{notes.length === 0 ? (
<p>Заметок пока нет</p>
) : (
<ul>
{notes.map((n) => (
<li key={n.id}>{n.text}</li>
))}
</ul>
)}
Подробнее — справочник React, обзор — 27.md.
React Router v6 — минимум
Один компонент App на весь сайт хватает для учебы. Для страниц /about, /notes подключают react-router-dom:
npm install react-router-dom
import { BrowserRouter, Routes, Route, Link, NavLink } from 'react-router-dom';
import Home from './pages/Home';
import About from './pages/About';
import NotFound from './pages/NotFound';
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="/users/:id" element={<UserProfile />} />
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
);
}
| Элемент | Назначение |
|---|---|
BrowserRouter | Связывает URL в адресной строке с деревом React |
Routes / Route | Какой компонент показать при каком path |
Link / NavLink | Навигация без полной перезагрузки страницы |
:id в path | Динамический сегмент; в компоненте — useParams() |
path="*" | Страница 404 для неизвестных URL |
Для SEO и серверного HTML чаще берут Next.js — там маршруты из структуры папок app/.
В vite.config.js можно настроить server.proxy: запрос /api/notes уходит на localhost:3000/notes. Подробнее: Fullstack 264.
Вынести счётчик в дочерний компонент
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>
);
}
Разбор:
- Компонент получает всё через props и не хранит собственный
count. valueотвечает только за отображение текущего состояния на экране.- Колбэки
onIncrement,onDecrement,onResetделегируют изменение состояния родителю. - Такой контракт делает
Counterпереиспользуемым и простым для тестирования.
В App.jsx state остаётся в родителе — подъём состояния (lifting state up):
<Counter
value={count}
onIncrement={() => setCount(count + 1)}
onDecrement={() => setCount(count - 1)}
onReset={() => setCount(0)}
/>
Разбор:
- Родитель остаётся владельцем
count, поэтому поток данных идёт сверху вниз. value={count}передаёт дочернему компоненту текущее значение без мутаций.- Функции в
onIncrement/onDecrement/onResetописывают разрешённые действия над состоянием. - Этот паттерн называется lifting state up и помогает синхронизировать несколько дочерних компонентов.
Композиция главной страницы из секций
Если нужно показать архитектуру лендинга (а не только счётчик), удобно вынести каждый блок в отдельный компонент и собрать страницу в корневом App.jsx:
import { TopBar } from './components/layout/TopBar';
import { BottomBar } from './components/layout/BottomBar';
import { IntroSection } from './components/sections/IntroSection';
import { ValueStrip } from './components/sections/ValueStrip';
import { BenefitsSection } from './components/sections/BenefitsSection';
import { WorkflowSection } from './components/sections/WorkflowSection';
import { TrustSection } from './components/sections/TrustSection';
import { PriceSection } from './components/sections/PriceSection';
import { QuestionsSection } from './components/sections/QuestionsSection';
import { FinalAction } from './components/sections/FinalAction';
export default function App() {
return (
<>
<TopBar />
<main>
<IntroSection />
<ValueStrip />
<BenefitsSection />
<WorkflowSection />
<TrustSection />
<PriceSection />
<QuestionsSection />
<FinalAction />
</main>
<BottomBar />
</>
);
}
Разбор:
- Блок
importзадаёт композицию страницы из независимых секций. Appвыступает оркестратором: собирает layout и порядок отображения контента.- Фрагмент
<>...</>группирует несколько корневых узлов без лишнего DOM-элемента. - Разделение на секции снижает связность и упрощает параллельную работу команды.
Такой подход упрощает поддержку: одна секция — один файл и одна зона ответственности.
Классовые компоненты
До появления хуков писали class App extends Component и this.setState. В новых проектах используют функции и хуки; классы остаются в legacy-коде — см. справочник React.
Частые ошибки
| Симптом | Причина | Что сделать |
|---|---|---|
Too many re-renders | setCount(...) в теле компонента | Вызывать setter только в обработчике или useEffect |
| Кнопка срабатывает при загрузке | onClick={handle()} | Передать функцию: onClick={() => handle()} |
| Список сбивается при удалении | Нет key или key={index} при перестановке | Стабильный key из данных (id) |
Бесконечный цикл в useEffect | В зависимостях объект/массив, создаваемый заново каждый рендер | Уточнить deps или мемоизировать значение |
fetch failed / CORS | API выключен или нет CORS | 264, прокси в Vite |
| State не меняется после setState | Мутация массива/объекта | Новая ссылка: setItems([...items, newItem]) |
| Прыгает фокус в форме | Прямая работа с DOM вместо state | Контролируемый input + value / onChange |
Полный список — справочник.
Что попробовать
- POST-заметку:
fetchсmethod: 'POST'иJSON.stringify({ text })— с блокировкой кнопки на время запроса: 45.md. - Next.js 2731 — тот же UI в
app/counter/page.tsxс'use client'. - Electron 118 — React в окне десктопа.
Мини-проект для закрепления React
После базового счётчика лучше сразу сделать маленький законченный сценарий. Он связывает форму, список, API и состояние в одном упражнении.
Идеи мини-проектов
| Уровень | Проект | Что тренируете |
|---|---|---|
| Начальный | Todo (список дел) | useState, .map(), key, фильтр все / активные |
| Начальный | Калькулятор | Несколько полей state, обработчики кнопок |
| Начальный | Заметки / блокнот | Форма + список, локально или через API |
| Начальный | Форма входа | Валидация, controlled inputs, сообщения об ошибке |
| Средний | Погода / Movie Search | useEffect, fetch, loading и error |
| Средний | Тёмная / светлая тема | Context или useState + localStorage |
| Средний | Поиск с фильтром | Фильтрация массива в рендере, debounce ввода |
| Средний | Пагинация по API | Страницы, query-параметры, кнопки «назад / вперёд» |
| Средний | Адаптивная шапка | Router + условные классы, NavLink |
Главное задание раздела — проект "Заметки" ниже: форма, CRUD и работа с API.
Задание "Заметки"
- Поле ввода
textи кнопка "Добавить". - Список заметок из API.
- Кнопка удаления каждой заметки.
- Индикатор загрузки и обработка ошибок.
Архитектура компонентов
App
NoteForm
NotesList
NoteItem
Разбор:
- Это дерево показывает иерархию компонентов и направление передачи props.
Appхранит общее состояние и бизнес-операции.NoteFormотвечает за ввод и отправку данных.NotesListрендерит коллекцию, аNoteItemинкапсулирует отображение/действия для одной заметки.
Что проверить вручную
- Пустой текст не отправляется.
- После успешного POST список обновляется без перезагрузки страницы.
- После DELETE карточка исчезает и интерфейс остаётся консистентным.
- При выключенном API показывается понятная ошибка.
Связанные материалы
| Тема | Материал |
|---|---|
| Галерея компонентов | React — компоненты-рецепты |
| Кнопка с загрузкой | 45.md |
| Обзор React | 27.md |
| Node API | 262.md · 263.md |
| Fullstack | 264.md |
| TypeScript + React | 30.md |
| Мобильный UI (Dart) | Flutter · виджеты (Lab) |
| Тесты компонентов | 33.md |
Базовый разбор HTTP и HTTPS находится в отдельной статье — HTTP как основа веб-интеграций.