Перейти к основному содержимому

Первая программа на React

Разработчику Архитектору
Загрузка симулятора первой программы…

Первая программа на React

Где применяют React

React — библиотека для пользовательского интерфейса. Вы описываете экран компонентами (функциями, которые возвращают разметку). Когда меняется состояние (число в счётчике, текст в поле), React пересчитывает, что показать, и обновляет DOM точечно — без полной перезагрузки страницы.

Маршруты вроде /about и HTTP-сервер подключают отдельно — React Router или Next.js. Сам React отвечает за экран: один компонент, один URL через Vite, через минуты виден результат.

В этой статье соберём:

  1. Счётчик и поле имени — useState
  2. Заголовок вкладки от счётчика — useEffect
  3. Список заметок с Node APIfetch

Сравнение: Vue · Angular · Next.js. Склейка с API: 264. Обзор: 27.md. Галерея готовых компонентов с разборомReact — компоненты-рецепты. Аналог для Vue и Svelte — галерея компонентов.


Что получится

ЧастьРезультат
ПроектVite + React на http://localhost:5173
UIСчётчик, ввод имени, список с сервера
APIGET 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=&#123;name&#125; + onChange=... делают поле ввода контролируемым компонентом.
  • Условие {name && <h2>... рендерит приветствие только при непустом значении.
  • Обработчики onClick передают функции, поэтому обновление происходит по событию, а не во время рендера.

Разбор по строкам

КодСмысл
useState(0)React создаёт ячейку памяти: значение 0 и функцию обновления
const [count, setCount]Деструктуризация: count — текущее, setCount — "записать новое"
setCount(count + 1)После вызова React заново вызывает App с новым count
value=&#123;name&#125;Input подчинён state: отображается то, что в React
onChange=&#123;(e) => setName(e.target.value)&#125;При вводе читаем текст из DOM и кладём в state
&#123;name && <h2>…&#125;Если name пустой — блок не рисуется
onClick=&#123;() => setCount(count + 1)&#125;В onClick передаём функцию. onClick=&#123;setCount(1)&#125; вызовет её сразу при рендере — ошибка

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=&#123;n.id&#125; помогает React корректно сопоставлять узлы.
СтрокаСмысл
useState([])Начальный список пустой
useEffect(..., [])Загрузить один раз при появлении компонента
.then(setNotes)setNotes получит распарсенный массив
key=&#123;n.id&#125;Стабильный ключ для списка между рендерами

Галерея типовых запросов с построчным разбором — GET, POST, Bearer, таймаут и React useEffect: Fetch / axios — типовые запросы. Проверка API в терминале — curl / fetch — примеры.

Подключите в App.jsx: <NotesList /> под счётчиком.


Условный рендеринг и списки

Интерфейс почти всегда зависит от данных: показать загрузку, пустой список или карточки.

ПриёмПримерКогда
&&&#123;isLoading && <Spinner />&#125;Показать блок, если условие истинно
Тернарный&#123;error ? <p>&#123;error&#125;</p> : <List />&#125;Два варианта разметки
.map()&#123;items.map((x) => <Row key=&#123;x.id&#125; … />)&#125;Список однотипных элементов

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/.

Прокси в dev

В 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=&#123;count&#125; передаёт дочернему компоненту текущее значение без мутаций.
  • Функции в 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-renderssetCount(...) в теле компонентаВызывать setter только в обработчике или useEffect
Кнопка срабатывает при загрузкеonClick=&#123;handle()&#125;Передать функцию: onClick=&#123;() => handle()&#125;
Список сбивается при удаленииНет key или key=&#123;index&#125; при перестановкеСтабильный key из данных (id)
Бесконечный цикл в useEffectВ зависимостях объект/массив, создаваемый заново каждый рендерУточнить deps или мемоизировать значение
fetch failed / CORSAPI выключен или нет CORS264, прокси в Vite
State не меняется после setStateМутация массива/объектаНовая ссылка: setItems([...items, newItem])
Прыгает фокус в формеПрямая работа с DOM вместо stateКонтролируемый input + value / onChange

Полный список — справочник.


Что попробовать

  1. POST-заметку: fetch с method: 'POST' и JSON.stringify(&#123; text &#125;) — с блокировкой кнопки на время запроса: 45.md.
  2. Next.js 2731 — тот же UI в app/counter/page.tsx с 'use client'.
  3. Electron 118 — React в окне десктопа.

Мини-проект для закрепления React

После базового счётчика лучше сразу сделать маленький законченный сценарий. Он связывает форму, список, API и состояние в одном упражнении.


Идеи мини-проектов

УровеньПроектЧто тренируете
НачальныйTodo (список дел)useState, .map(), key, фильтр все / активные
НачальныйКалькуляторНесколько полей state, обработчики кнопок
НачальныйЗаметки / блокнотФорма + список, локально или через API
НачальныйФорма входаВалидация, controlled inputs, сообщения об ошибке
СреднийПогода / Movie SearchuseEffect, fetch, loading и error
СреднийТёмная / светлая темаContext или useState + localStorage
СреднийПоиск с фильтромФильтрация массива в рендере, debounce ввода
СреднийПагинация по APIСтраницы, query-параметры, кнопки «назад / вперёд»
СреднийАдаптивная шапкаRouter + условные классы, NavLink

Главное задание раздела — проект "Заметки" ниже: форма, CRUD и работа с API.


Задание "Заметки"

  • Поле ввода text и кнопка "Добавить".
  • Список заметок из API.
  • Кнопка удаления каждой заметки.
  • Индикатор загрузки и обработка ошибок.

Архитектура компонентов

App
NoteForm
NotesList
NoteItem

Разбор:

  • Это дерево показывает иерархию компонентов и направление передачи props.
  • App хранит общее состояние и бизнес-операции.
  • NoteForm отвечает за ввод и отправку данных.
  • NotesList рендерит коллекцию, а NoteItem инкапсулирует отображение/действия для одной заметки.

Что проверить вручную

  1. Пустой текст не отправляется.
  2. После успешного POST список обновляется без перезагрузки страницы.
  3. После DELETE карточка исчезает и интерфейс остаётся консистентным.
  4. При выключенном API показывается понятная ошибка.

Связанные материалы

ТемаМатериал
Галерея компонентовReact — компоненты-рецепты
Кнопка с загрузкой45.md
Обзор React27.md
Node API262.md · 263.md
Fullstack264.md
TypeScript + React30.md
Мобильный UI (Dart)Flutter · виджеты (Lab)
Тесты компонентов33.md

Основа по протоколу

Базовый разбор HTTP и HTTPS находится в отдельной статье — HTTP как основа веб-интеграций.