React с нуля: от теории к работающему коду

Практический курс по React для разработчиков, владеющих HTML, CSS и JavaScript. С первых глав вы пишете код: компоненты, JSX, состояние, пропсы, события — и завершаете курс созданием работающего интерактивного интерфейса.

1. Основы React и JSX

Основы React и JSX

Почему миллионы разработчиков выбрали React для создания интерфейсов, хотя чистый JavaScript тоже умеет рисовать кнопки и формы? Ответ прост: React убирает рутину ручного обновления страницы и позволяет думать о пользовательском интерфейсе как о наборе переиспользуемых деталей конструктора. Если вы уверенно пишете на HTML, CSS и JavaScript, но никогда не трогали React — эта статья переведёт вас от теории к первому работающему коду за 15 минут.

Что такое React на самом деле

React — это JavaScript-библиотека для построения пользовательских интерфейсов. Не фреймворк, не платформа, а именно библиотека: она решает одну задачу — отрисовку UI и его обновление при изменении данных. Всё остальное (роутинг, работа с сервером, управление формами) вы подключаете отдельно по мере необходимости.

Ключевая идея React — декларативный подход. Вы описываете, как должен выглядеть интерфейс при определённых данных, а React сам разбирается, как обновить страницу. Сравните два подхода:

| Императивный (чистый JS) | Декларативный (React) | |---|---| | Найди элемент по id | Опиши, что должно отобразиться | | Измени его текстовое содержимое | React сам обновит DOM | | Удали старый класс, добавь новый | Просто укажи нужные данные |

Представьте навигатор: императивный подход — это пошаговая инструкция «поверни направо, проедь 200 метров, поверни налево». Декларативный — вы просто указываете пункт назначения, а навигатор строит маршрут сам.

Установка окружения

Перед написанием кода нужно подготовить инструменты. Понадобятся три вещи:

  • Node.js — среда выполнения JavaScript вне браузера. Скачайте LTS-версию с nodejs.org.
  • npm — менеджер пакетов, идёт в комплекте с Node.js.
  • Редактор кода — рекомендую Visual Studio Code с расширением ES7+ React/Redux snippets.
  • Проверьте установку в терминале:

    Если обе команды вернули номера версий — окружение готово.

    Создание первого проекта

    Самый быстрый способ получить работающий React-проект — утилита Create React App. Она настраивает Webpack, Babel, dev-сервер и прочее, чтобы вы могли сразу писать код.

    После выполнения команд браузер автоматически откроет страницу по адресу http://localhost:3000. Вы увидите вращающийся логотип React — это значит, что приложение работает.

    Структура созданного проекта выглядит так:

    Всё, что касается пользовательского интерфейса, живёт в папке src/. Файлы в public/ трогать на первых порах не нужно.

    JSX — HTML внутри JavaScript

    Откройте файл src/App.js. Вы увидите код, который выглядит как HTML, но находится внутри JavaScript-функции:

    Это JSX — синтаксическое расширение JavaScript, которое позволяет писать HTML-подобную разметку прямо внутри JS-кода. JSX не является обязательным, но на практике его используют в 99% React-проектов.

    Правила JSX

    JSX похож на HTML, но имеет несколько важных отличий:

    Один корневой элемент. Функция компонента должна возвращать один общий контейнер. Если нужно вернуть несколько элементов, оберните их в <div> или пустой тег <>...</> (это React.Fragment):

    Атрибуты на JavaScript-манер. В JSX зарезервированные слова JavaScript заменяются аналогами:

  • classclassName
  • forhtmlFor
  • tabindextabIndex
  • Это потому, что JSX транслируется в вызовы React.createElement(), а class в JavaScript — ключевое слово.

    Вставка выражений через фигурные скобки. Внутри {} можно размещать любые JavaScript-выражения: переменные, вызовы функций, арифметические операции:

    Обратите внимание: внутри {} можно писать только выражения (то, что возвращает значение). Инструкции вроде if, for напрямую в JSX не работают — их нужно выносить за пределы return или заменять тернарным оператором.

    JSX под капотом

    Когда вы пишете <h1>Привет</h1>, компилятор Babel превращает это в вызов:

    Знание этого факта помогает понять, почему JSX имеет именно такие ограничения. Но на практике вам не нужно писать createElement вручную — JSX делает код читаемым и компактным.

    Первое изменение: сделаем код своим

    Замените содержимое App.js на следующее:

    Сохраните файл — браузер обновится автоматически благодаря hot reload (горячей перезагрузке). Вы увидите заголовок и список технологий.

    Обратите внимание на атрибут key внутри map. React использует его, чтобы отслеживать элементы списка при обновлении. Без key при изменении данных React будет перерисовывать весь список целиком вместо точечного обновления. На данном этапе достаточно передать index, но в реальных проектах лучше использовать уникальные идентификаторы.

    Как React обновляет страницу

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

  • React создаёт новое виртуальное дерево на основе обновлённых данных.
  • Сравнивает его с предыдущей версией (diffing).
  • Вычисляет минимальный набор изменений.
  • Применяет только эти изменения к реальному DOM.
  • Этот процесс называется reconciliation. Благодаря ему React обновляет страницу быстро, даже если интерфейс состоит из сотен элементов.

    Сборка для продакшена

    Когда приложение готово к публикации, выполните:

    React создаст папку build/ с оптимизированными файлами: минифицированный JavaScript, сжатый CSS, подготовленный HTML. Эти файлы можно загрузить на любой хостинг.

    Типичные ошибки начинающих

  • Забыть export default — без экспорта компонент недоступен другим файлам.
  • Использовать class вместо className — браузер не упадёт, но в консоли появится предупреждение.
  • Вернуть два элемента без обёртки — JSX требует один корневой элемент.
  • Забыть фигурные скобки при вставке переменных — текст {userName} выведется буквально, а не значение переменной.
  • Теперь у вас есть работающее React-приложение, вы понимаете, что такое JSX и как он работает. В следующей статье мы разберём, как разбивать интерфейс на компоненты и передавать данные между ними через пропсы.

    2. Компоненты и передача данных через пропсы

    Компоненты и передача данных через пропсы

    Представьте, что вы строите дом из LEGO. У вас есть кирпичики разных форм и размеров, и из них можно собрать что угодно — замок, машину, ракету. React-компоненты работают точно так же: это переиспользуемые блоки интерфейса, из которых собираются страницы любой сложности. Но кирпичики бесполезны, если между ними нет соединений. В React роль таких соединений выполняют пропсы — механизм передачи данных от одного компонента к другому.

    Функциональные компоненты

    В современном React компонент — это обычная JavaScript-функция, которая возвращает JSX. Никаких классов, никакого наследования. Просто функция:

    Правила именования компонентов строгие: имя должно начинаться с заглавной буквы. React отличает компоненты от HTML-тегов именно по регистру: <div> — это HTML-элемент, а <Greeting> — компонент.

    Чтобы использовать компонент, вставьте его как тег в JSX другого компонента:

    Компонент Greeting отобразится три раза. Это и есть переиспользуемость: определили один раз — используем сколько угодно.

    Разделение на файлы

    Когда компонентов становится много, держать их всех в одном файле неудобно. Каждый компонент принято выносить в отдельный файл. Создайте папку src/components/ и файл Greeting.js:

    Затем импортируйте его в App.js:

    Обратите внимание на путь: ./components/Greeting — без расширения .js. Create React App настроен так, что расширение можно опускать.

    Пропсы: передача данных в компонент

    Сейчас компонент Greeting всегда показывает один и тот же текст. Чтобы сделать его гибким, нужно передавать данные извне. Для этого служат пропсы (от properties — свойства).

    Пропсы — это объект, который React автоматически передаёт в функцию компонента первым аргументом. Значения пропсов задаются как атрибуты при использовании компонента:

    Результат в браузере: Привет, Алексей!

    Теперь один и тот же компонент может выводить разные приветствия:

    Деструктуризация пропсов

    Обращение через props.name работает, но становится громоздким при большом количестве пропсов. Практический стандарт — деструктуризация прямо в параметрах функции:

    Обратите внимание на синтаксис передачи числовых значений: {28} в фигурных скобках. Без скобок React воспримет "28" как строку. Строковые значения передаются в кавычках, всё остальное — в {}.

    Передача разных типов данных

    Пропсы могут принимать любой тип JavaScript:

    Особенно важна передача функций — это основной способ связать родительский и дочерний компоненты. Дочерний компонент вызывает переданную функцию, а родительский определяет, что произойдёт при этом вызове.

    Вложенные компоненты и children

    Компоненты можно вкладывать друг в друга. Содержимое между открывающим и закрывающим тегом компонента попадает в специальный проп children:

    children — это мощный инструмент для создания обёрток и layout-компонентов. Всё, что вы размещаете между <Card> и </Card>, автоматически становится значением children.

    Практический пример: карточки товаров

    Соберём компоненты в реальный сценарий — каталог товаров:

    Каждый ProductCard получает данные через пропсы и отображает их. Если завтра появится новый товар — достаточно добавить объект в массив products.

    Пропсы доступны только для чтения

    Важнейшее правило: компонент не может изменять свои пропсы. Это односторонний поток данных — от родителя к ребёнку. Если попытаться изменить проп внутри компонента, React выдаст ошибку:

    Если компоненту нужно изменять данные — для этого существует состояние, которое мы рассмотрим в следующей статье. Пропсы — это входные данные, которые компонент получает и отображает, но не трогает.

    Валидация пропсов

    В больших проектах полезно проверять, что компонент получает пропсы правильного типа. Для этого существует библиотека prop-types (установка: npm install prop-types):

    Если передать name не строкой или не передать вообще — в консоли появится предупреждение. defaultProps задаёт значения по умолчанию для необязательных пропсов.

    Значения по умолчанию через параметры

    Более современный способ задать значения по умолчанию — стандартные параметры JavaScript в деструктуризации:

    Если проп age не передан, подставится 0. Это работает точно так же, как и с обычными функциями JavaScript.

    Теперь вы умеете создавать компоненты, разделять их на файлы и передавать данные через пропсы. Следующий шаг — научить компоненты хранить и изменять собственные данные с помощью состояния.

    3. Управление состоянием с помощью useState

    Управление состоянием с помощью useState

    Компоненты и пропсы позволяют отображать данные, но что делать, если интерфейс должен реагировать на действия пользователя? Кнопка счётчика, форма с вводом текста, переключатель темы — всё это требует, чтобы компонент помнил что-то между перерисовками. Именно для этого в React существует состояние (state), а управлять им помогает хук useState.

    Зачем нужно состояние

    Пропсы — это данные, которые компонент получает от родителя. Они неизменяемы внутри компонента. Но представьте текстовое поле: когда пользователь печатает, компонент должен запоминать введённый текст. Это внутренние данные компонента, которые меняются со временем, — и есть состояние.

    Простое правило для определения, нужен ли useState:

  • Данные приходят извне и не меняются внутри компонента → пропсы
  • Данные меняются внутри компонента (по клику, по вводу, по таймеру) → состояние
  • Хук useState: базовый синтаксис

    Хуки — это специальные функции React, которые позволяют функциональным компонентам использовать возможности, ранее доступные только классовым компонентам. useState — первый и самый часто используемый хук.

    Разберём строку const [count, setCount] = useState(0):

  • useState(0) — вызов хука с начальным значением 0. Возвращает массив из двух элементов.
  • count — текущее значение состояния. При первом рендере равно 0.
  • setCount — функция для обновления состояния. Когда вы вызываете setCount(5), React записывает новое значение и перерисовывает компонент.
  • Деструктуризация [count, setCount] — стандартный синтаксис JavaScript для извлечения элементов из массива.
  • Имена count и setCount — произвольные, но принято называть функцию-сеттер как set + имя переменной.

    Как работает перерисовка

    Когда вы вызываете setCount(count + 1), происходит следующее:

  • React запоминает новое значение состояния.
  • React заново вызывает функцию компонента.
  • Возвращается новый JSX с обновлёнными данными.
  • React сравнивает новый и старый виртуальный DOM и применяет минимальные изменения.
  • Ключевой момент: при перерисовке вся функция компонента выполняется заново. Все переменные, объявленные внутри функции, создаются с нуля. Состояние — единственное, что сохраняется между перерисовками.

    Несколько состояний в одном компоненте

    Вы можете вызывать useState столько раз, сколько нужно. Каждый вызов создаёт независимое состояние:

    Каждое состояние независимо: изменение name не влияет на age или isOnline. Это проще и читаемее, чем один большой объект состояния.

    Обновление на основе предыдущего значения

    Есть тонкий момент, который ловит многих начинающих. Посмотрите на этот код:

    Сколько раз увеличится счётчик при одном клике? Один, а не два. Потому что оба вызова setCount используют одно и то же значение count, которое ещё не обновилось. React собирает обновления и применяет их пакетом.

    Чтобы гарантированно использовать актуальное предыдущее значение, передайте в setCount функцию:

    Теперь при одном клике счётчик увеличится на 2. Функция prev => prev + 1 получает предыдущее состояние и возвращает новое. Это функциональное обновление состояния, и его рекомендуется использовать всегда, когда новое значение зависит от предыдущего.

    Состояние с объектами и массивами

    useState может хранить любой тип данных, но при работе с объектами и массивами есть важное правило: нельзя изменять существующий объект или массив напрямую. Нужно создавать новый:

    Почему нельзя изменять объект напрямую? React определяет, нужно ли перерисовывать компонент, по ссылке на объект. Если вы измените свойство существующего объекта, ссылка не изменится — React не увидит разницы и не обновит интерфейс. Создание нового объекта ({ ...todo, done: !todo.done }) гарантирует новую ссылку.

    | Операция | Неправильно (мутация) | Правильно (иммутабельность) | |---|---|---| | Добавить в массив | arr.push(item) | [...arr, item] | | Удалить из массива | arr.splice(i, 1) | arr.filter((_, idx) => idx !== i) | | Изменить объект | obj.name = 'X' | { ...obj, name: 'X' } | | Изменить вложенное | obj.address.city = 'X' | { ...obj, address: { ...obj.address, city: 'X' } } |

    Ленивая инициализация состояния

    Если начальное значение требует сложных вычислений (например, чтение из localStorage), передайте функцию в useState. React вызовет её только при первом рендере:

    Когда состояние обновляется

    Вызов setCount(5) не меняет count мгновенно. React планирует обновление и выполняет его при следующем рендере. Это значит, что сразу после setCount переменная count всё ещё содержит старое значение:

    Это нормальное поведение. Если вам нужно выполнить действие после обновления состояния, используйте хук useEffect, который мы рассмотрим в следующих статьях.

    Практический пример: переключатель темы

    Компонент хранит булево состояние isDark и на его основе вычисляет стили. При клике состояние инвертируется, React перерисовывает компонент с новыми стилями.

    Теперь вы умеете заставлять компоненты запоминать и изменять данные. В следующей статье мы подробнее разберём обработку событий — как реагировать на клики, ввод текста и другие действия пользователя.

    4. Обработка событий и интерактивность

    Обработка событий и интерактивность

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

    Система событий React

    React не привязывает обработчики напрямую к DOM-элементам. Вместо этого он использует 合成事件 (Synthetic Events) — обёртку над нативными событиями браузера. Это даёт два преимущества:

  • Кроссбраузерность — API событий одинаковый во всех браузерах.
  • Делегирование — React вешает один обработчик на корневой элемент, а не по одному на каждый элемент. Это экономит память при большом количестве элементов.
  • На практике вам не нужно знать эти детали — API событий в React выглядит почти так же, как в обычном HTML.

    Базовый синтаксис обработки событий

    В HTML обработчик пишется как строка: <button onclick="handleClick()">. В React обработчик — это функция, переданная через проп с префиксом on:

    Обратите внимание на отличия от HTML:

  • onclickonClick (camelCase)
  • Строка "handleClick()" → функция {handleClick}
  • Скобки после имени функции в JSX вызовут её при рендере, а не при клике
  • Частая ошибка новичков:

    Объект события

    Обработчик receives объект события — аналог event в нативном JavaScript. Из него можно извлечь информацию о действии пользователя:

    Метод event.preventDefault() — один из самых часто используемых. По умолчанию отправка формы перезагружает страницу. В React-приложении это нежелательно, поэтому отправку формы нужно предотвращать и обрабатывать данные через JavaScript.

    Работа с формами и controlled components

    Формы — самый частый источник событий в веб-приложениях. React предлагает паттерн controlled components, при котором значение каждого поля формы хранится в состоянии компонента:

    Здесь каждое поле — controlled component: его значение определяется состоянием React, а не внутренним состоянием DOM. Пользователь вводит текст → срабатывает onChange → обновляется состояние → React перерисовывает поле с новым значением.

    Преимущества controlled components:

  • Валидация в реальном времени
  • Возможность форматировать ввод (например, автоматически добавлять пробелы в номер карты)
  • Единый источник правды — все данные формы всегда в состоянии
  • Обработка разных типов полей

    Здесь используется один обработчик handleChange для всех полей. Он определяет тип поля через event.target.type и извлекает значение соответственно: для чекбокса — checked, для остальных — value. Имя поля берётся из атрибута name и используется как ключ в объекте состояния.

    Популярные события

    React поддерживает все стандартные DOM-события. Вот самые используемые:

    | Событие | Когда срабатывает | Типичное применение | |---|---|---| | onClick | Клик мыши | Кнопки, ссылки, выбор элемента | | onChange | Изменение значения поля | Формы, фильтры, поиск | | onSubmit | Отправка формы | Обработка данных формы | | onKeyDown / onKeyUp | Нажатие / отпускание клавиши | Горячие клавиши, навигация | | onFocus / onBlur | Получение / потеря фокуса | Валидация, подсказки | | onMouseEnter / onMouseLeave | Наведение / уход мыши | Тултипы, подсветка | | onScroll | Прокрутка элемента | Ленивая загрузка, анимации |

    Передача данных в обработчики

    Часто обработчику нужны дополнительные данные — например, id элемента, по которому кликнули. Есть несколько способов:

    Стрелочная функция в JSX:

    Метод bind:

    Первый способ проще и используется чаще. Второй чуть производительнее при очень большом количестве элементов.

    Практический пример: интерактивный список с фильтрацией

    Соберём компонент, который объединяет формы, события и состояние:

    Этот компонент демонстрирует ключевые приёмы: controlled input для поиска, обработку onClick для переключения элементов, onKeyDown для сброса поиска по Escape, и вычисляемое значение filteredItems на основе состояния.

    Частые ошибки при обработке событий

  • Вызов функции вместо передачи ссылкиonClick={handleClick()} сработает при рендере, а не при клике.
  • Забыть event.preventDefault() — форма перезагрузит страницу.
  • Мутировать состояние напрямуюitems[0].done = true не вызовет перерисовку.
  • Создавать обработчики внутри render без необходимости — если функция не зависит от пропсов или состояния, вынесите её за пределы компонента.
  • Теперь вы умеете обрабатывать клики, ввод текста, отправку форм и нажатия клавиш. В следующей статье мы соберём всё вместе и создадим полноценный интерактивный интерфейс.

    5. Практика: создание интерактивного интерфейса

    Практика: создание интерактивного интерфейса

    Вы прошли путь от первого JSX-выражения до обработки сложных событий. Теперь пришло время собрать все концепции воедино и построить реальное приложение — список задач с фильтрацией, поиском и статистикой. Это классический сценарий, который встречается в продакшене: он задействует компоненты, пропсы, состояние, обработку событий и работу с массивами. Каждый шаг — это код, который вы можете запустить и проверить.

    Архитектура приложения

    Прежде чем писать код, определим структуру. Наше приложение будет состоять из четырёх компонентов:

    Appединственный источник правды. Вся информация о задачах хранится в его состоянии, а дочерние компоненты получают данные через пропсы и отправляют изменения через callback-функции. Это называется подъём состояния (lifting state up) — стандартный паттерн React.

    Шаг 1: Создание проекта

    Очистите src/App.js и src/App.css. Мы будем писать с чистого листа.

    Шаг 2: Компонент TodoItem

    Начнём с атомарного компонента — отдельной задачи. Он получает данные через пропсы и вызывает callback-функции при действиях пользователя:

    Массив filters описывает доступные опции. Если завтра понадобится новый фильтр — достаточно добавить объект в массив.

    Шаг 6: Компонент TodoStats

    Статистика вычисляется на основе переданных данных:

    Компонент ничего не хранит — он только вычисляет и отображает. Это чистый презентационный компонент.

    Шаг 7: Главный компонент App

    Теперь соберём всё вместе. App хранит состояние и содержит логику фильтрации:

    Обратите внимание: filteredTodos — это вычисляемое значение, а не отдельное состояние. Оно пересчитывается при каждом рендере на основе todos, filter и search. Хранить отфильтрованный список в состоянии было бы ошибкой — данные дублировались бы и рассинхронизировались.

    Шаг 8: Стили

    Добавьте в src/App.css базовые стили:

    Поток данных в приложении

    Вот как информация движется по компонентам:

  • Пользователь вводит текст в TodoForm → вызывается onAdd(text)App добавляет задачу в todos.
  • Пользователь кликает чекбокс в TodoItem → вызывается onToggle(id)App обновляет todos.
  • Пользователь меняет фильтр в TodoFilters → вызывается onFilterChange(value)App обновляет filter.
  • При рендере App вычисляет filteredTodos и передаёт в TodoList.
  • TodoStats получает полный todos и отображает статистику.
  • Данные всегда текут сверху вниз (через пропсы), а события — снизу вверх (через callback-функции). Это однонаправленный поток данных — фундаментальный принцип React.

    Что дальше

    Вы построили работающее приложение с нуля. Вот что можно добавить для практики:

  • Сохранение в localStorage — чтобы задачи не пропадали при перезагрузке. Используйте useEffect для синхронизации состояния с localStorage.
  • Редактирование задач — по двойному клику текст становится полем ввода.
  • Сортировка — по дате, по алфавиту, по статусу.
  • Анимации — плавное появление и удаление задач с помощью CSS-переходов или библиотеки Framer Motion.
  • Каждая из этих задач задействует концепции, которые вы уже изучили: компоненты, пропсы, состояние и обработка событий. React — это не набор магических правил, а набор простых паттернов, которые комбинируются для создания интерфейсов любой сложности.