React — библиотека для пользовательских интерфейсов. Его главная идея: интерфейс описывается как функция от состояния приложения.

Вместо того чтобы вручную управлять DOM:

  • искать элементы
  • менять текст
  • добавлять классы
  • синхронизировать состояние

React предлагает описать, как интерфейс должен выглядеть при текущем состоянии. Всё остальное библиотека делает сама.

Упрощённая формула React:

UI = f(state)

Если состояние меняется — интерфейс автоматически перерисовывается.

Почему появился React

До React интерфейсы часто писались так:

const button = document.querySelector("#status")

if (user.online) {
  button.textContent = "online"
  button.classList.add("green")
} else {
  button.textContent = "offline"
  button.classList.add("gray")
}

Проблема такого подхода — логика интерфейса быстро превращается в хаос:

  • состояние разбросано по коду
  • DOM постоянно мутируется
  • сложно поддерживать

React предлагает другой подход: описать интерфейс декларативно.

Декларативный UI

В React мы просто говорим: “Если пользователь онлайн — покажи зелёный статус. Если офлайн — серый.”

function StatusBadge({ online }) {
  return online
    ? <span className="badge badge-green">online</span>
    : <span className="badge badge-gray">offline</span>
}

Теперь React сам решает:

  • когда обновлять DOM
  • что изменилось
  • какие элементы нужно перерисовать

Разработчик работает с состоянием, а не с DOM.

React — библиотека, а не фреймворк

React отвечает только за одну задачу: рендеринг интерфейса.

Он не диктует:

  • структуру проекта
  • роутинг
  • работу с API
  • управление состоянием

Поэтому вокруг React существует большая экосистема:

задача инструмент
сборка Vite
роутинг React Router
состояние Zustand / Redux
запросы TanStack Query
фреймворк Next.js

Это делает React очень гибким, но иногда требует больше решений от разработчика.

Где используется React

React используется для создания:

  • SPA приложений
  • админок
  • SaaS сервисов
  • интерфейсов стартапов
  • сложных веб-приложений

Крупные продукты, использующие React:

  • Facebook
  • Instagram
  • Airbnb
  • Netflix
  • Shopify

Также React стал основой многих фреймворков:

  • Next.js
  • Remix
  • Expo (React Native)

Когда React — плохой выбор

React — мощный инструмент, но он не всегда нужен. Иногда лучше выбрать что-то проще:

Обычный сайт (лендинг, блог, маркетинговая страница)
Лучше использовать: HTML + CSS, Astro, статический генератор

Очень маленький интерфейс (простой виджет, небольшая форма)
React может быть избыточным.

Как работает React под капотом

Чтобы эффективно писать React-код, важно понимать три вещи:

  • Virtual DOM
  • Reconciliation
  • Ререндеры компонентов

Virtual DOM

Virtual DOM — это лёгкая копия DOM в памяти.

React не работает напрямую с браузерным DOM, потому что операции с ним дорогие.

Вместо этого React:

  1. создаёт Virtual DOM
  2. сравнивает его с предыдущей версией
  3. обновляет только изменившиеся части настоящего DOM

Представим компонент:

function CartTotal({ items }) {
  const total = items.reduce((sum, item) => sum + item.price, 0)
  return <h2>Total: ${total}</h2>
}

Если корзина меняется:

items = [
  { price: 10 },
  { price: 15 }
]

React рендерит: Total: $25

Когда добавляется новый товар:

items = [
  { price: 10 },
  { price: 15 },
  { price: 5 }
]

React делает следующее:

  1. создаёт новый Virtual DOM
  2. сравнивает его со старым
  3. находит отличие: 25 → 30
  4. обновляет только текст внутри <h2>

DOM-элемент при этом не пересоздаётся.

Благодаря Virtual DOM минимизируются реальные изменения DOM, что ускоряет работу приложения.

Reconciliation

Процесс сравнения Virtual DOM называется reconciliation.

React анализирует:

  • какие элементы изменились
  • какие можно переиспользовать
  • какие нужно удалить или создать

Например:

<ul>
  <li>Apple</li>
  <li>Orange</li>
</ul>

После обновления:

<ul>
  <li>Apple</li>
  <li>Orange</li>
  <li>Banana</li>
</ul>

React понимает: первые два элемента совпадают, добавился один новый. Поэтому он просто добавляет один <li>, а не перерисовывает весь список.

Почему важны ключи (key)

Когда React рендерит списки, он должен понимать: какой элемент соответствует какому.

Поэтому используется key.

function Notifications({ notifications }) {
  return (
    <ul>
      {notifications.map(n => (
        <li key={n.id}>
          {n.message}
        </li>
      ))}
    </ul>
  )
}

key помогает React:

  • быстрее сравнивать списки
  • избегать лишних ререндеров
  • корректно обновлять элементы

Ререндер компонентов

Когда состояние меняется, React перерисовывает компонент.

Важно понять: рендер ≠ обновление DOM.

Рендер — это просто выполнение функции компонента.

Например:

function CartCounter({ items }) {
  return <span>{items.length}</span>
}

Если состояние изменилось: items.push(newItem)

React:

  1. снова вызывает функцию CartCounter
  2. создаёт новый Virtual DOM
  3. сравнивает его со старым
  4. обновляет DOM только если есть изменения

Почему React быстрый

Несмотря на ререндеры, React остаётся быстрым благодаря:

  1. Virtual DOM — минимизирует реальные изменения DOM.
  2. Батчингу обновлений — React объединяет несколько обновлений в одно.
  3. Умному алгоритму diff — React анализирует структуру дерева и обновляет только изменившиеся узлы.

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

Раньше React-проекты часто создавали с помощью Create React App, но сегодня стандартный способ — Vite.

Vite быстрее запускается, быстрее собирает проект и проще настраивается.

Создаём проект

В терминале выполните:

npm create vite@latest react-app

После этого перейдите в папку проекта:

cd react-app
npm install
npm run dev

После запуска Vite покажет адрес приложения:

http://localhost:5173

Откройте его в браузере — вы увидите стартовую страницу React.

Структура проекта

После создания проекта структура будет примерно такой:

react-app
 ├─ node_modules
 ├─ public
 ├─ src
 │   ├─ App.jsx
 │   ├─ main.jsx
 │   ├─ index.css
 │   └─ assets
 ├─ index.html
 └─ package.json

Основная работа происходит в папке src.

Точка входа приложения

Файл:

src/main.jsx

Он подключает React к странице.

import React from "react"
import ReactDOM from "react-dom/client"
import App from "./App.jsx"

ReactDOM.createRoot(document.getElementById("root")).render(
  <App />
)

Что здесь происходит:

  1. React находит элемент #root в HTML
  2. запускает приложение
  3. рендерит компонент App

Главный компонент

Файл:

src/App.jsx

Это основной компонент приложения.

Пример простого интерфейса:

function App() {
  return (
    <div>
      <h1>My first React app</h1>
      <p>React is running 🚀</p>
    </div>
  )
}

export default App

Когда вы сохраняете файл, Vite автоматически обновляет страницу.

Это называется Hot Reload.

Где писать код

Обычно проект постепенно разделяют на компоненты.

Например:

src
 ├─ components
 │   ├─ Header.jsx
 │   ├─ ProductCard.jsx
 │   └─ UserMenu.jsx
 ├─ pages
 │   ├─ Home.jsx
 │   └─ Profile.jsx
 └─ App.jsx

Так приложение становится легче поддерживать.

JSX — синтаксис React

Когда вы пишете React-компоненты, вы используете JSX.

JSX выглядит как HTML, но на самом деле это JavaScript.

Простой пример JSX

function App() {
  return <h1>Hello React</h1>
}

На первый взгляд это HTML. Но JSX превращается в JavaScript.

Пример того, во что компилируется JSX:

React.createElement("h1", null, "Hello React")

Vite и Babel делают это автоматически.

JSX может использовать JavaScript

Внутри JSX можно писать JavaScript выражения.

Для этого используются фигурные скобки.

function Welcome() {
  const name = "Anna"

  return <h2>Hello {name}</h2>
}

Результат:

Hello Anna

Динамический интерфейс

JSX часто используется вместе с условиями.

function Temperature({ value }) {
  return (
    <div>
      <p>Temperature: {value}°C</p>
      {value < 0 && <p>It's freezing ❄️</p>}
    </div>
  )
}

Если температура ниже нуля, появляется дополнительный текст.

В JSX должен быть один родительский элемент

Это неправильно:

return (
  <h1>Hello</h1>
  <p>React</p>
)

Правильно:

return (
  <div>
    <h1>Hello</h1>
    <p>React</p>
  </div>
)

Или можно использовать Fragment:

return (
  <>
    <h1>Hello</h1>
    <p>React</p>
  </>
)

Fragment не создаёт дополнительный элемент в DOM.

Атрибуты в JSX

Атрибуты похожи на HTML, но иногда отличаются.

Например:

class → className

Пример:

function Button() {
  return <button className="primary">Buy</button>
}

JSX можно разбивать на переменные

Это удобно, когда интерфейс становится сложнее.

function Product() {
  const price = 29

  const badge = price < 30
    ? <span>Cheap</span>
    : <span>Premium</span>

  return (
    <div>
      <h3>Product</h3>
      {badge}
    </div>
  )
}

Использование переменных для хранения JSX-фрагментов помогает сделать код более читаемым и управляемым.

Компоненты в React

Главная идея React — интерфейс собирается из компонентов.

Компонент — это независимый кусок интерфейса, который можно переиспользовать.

Например, в интернет-магазине можно выделить компоненты:

  • Header
  • ProductCard
  • Cart
  • Button
  • Footer

Каждый из них отвечает только за свою часть интерфейса.

Пример компонента:

function Button() {
  return <button>Buy</button>
}

Теперь этот компонент можно использовать в других местах:

function App() {
  return (
    <div>
      <Button />
      <Button />
      <Button />
    </div>
  )
}

React просто вызывает функцию Button каждый раз, когда встречает <Button />.

Переиспользование компонентов

Главное преимущество компонентов — повторное использование.

Вместо того чтобы копировать HTML, мы создаём компонент один раз.

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

function ProductCard() {
  return (
    <div className="product">
      <h3>Phone</h3>
      <p>$500</p>
      <button>Buy</button>
    </div>
  )
}

Использование:

function App() {
  return (
    <div>
      <ProductCard />
      <ProductCard />
      <ProductCard />
    </div>
  )
}

Но сейчас все карточки одинаковые.

Чтобы сделать их динамическими, используется props.

Props — параметры компонентов

Props — это данные, которые передаются в компонент.

Они работают как аргументы функции.

Пример:

function ProductCard(props) {
  return (
    <div className="product">
      <h3>{props.title}</h3>
      <p>${props.price}</p>
    </div>
  )
}

Использование:

function App() {
  return (
    <div>
      <ProductCard title="Phone" price={500} />
      <ProductCard title="Laptop" price={1200} />
      <ProductCard title="Tablet" price={300} />
    </div>
  )
}

Теперь один компонент может отображать разные данные.

Деструктуризация props

Обычно props сразу распаковывают:

function ProductCard({ title, price }) {
  return (
    <div className="product">
      <h3>{title}</h3>
      <p>${price}</p>
    </div>
  )
}

Это делает код короче и удобнее.

Props только для чтения

Важно понимать:

Props нельзя изменять внутри компонента.

Это только входные данные.

Неправильно:

function Counter({ value }) {
  value = value + 1
}

Правильно: изменения должны происходить в родительском компоненте.

React придерживается принципа одностороннего потока данных.

Parent → Child → Child → Child

Данные всегда передаются сверху вниз.

Children — вложенные элементы

В React можно передавать не только данные, но и другие элементы интерфейса.

Для этого используется children.

Пример:

function Card({ children }) {
  return (
    <div className="card">
      {children}
    </div>
  )
}

Использование:

function App() {
  return (
    <Card>
      <h3>Product</h3>
      <p>$50</p>
    </Card>
  )
}

React передаст всё, что находится между <Card> и </Card>, в children.

Состояние (State)

Props — это данные извне.

Но часто компоненту нужно хранить собственные данные.

Например:

  • открыто ли модальное окно
  • текст в поле ввода
  • количество товаров в корзине

Для этого используется state.

В React состояние создаётся с помощью хука useState.

Пример:

import { useState } from "react"

function Counter() {
  const [count, setCount] = useState(0)

  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>
        Increase
      </button>
    </div>
  )
}

Как работает useState

Строка:

const [count, setCount] = useState(0)

делает три вещи:

  1. создаёт состояние count
  2. задаёт начальное значение 0
  3. даёт функцию setCount для изменения состояния

Если вызвать:

setCount(count + 1)

React:

  1. обновит состояние
  2. заново вызовет компонент
  3. обновит интерфейс

Почему нельзя менять state напрямую

Нельзя делать так:

count = count + 1

React не узнает об изменении.

Правильный способ:

setCount(count + 1)

Обновлять состояние нужно только через функцию setState.

Ререндер при изменении state

Когда состояние меняется, React:

  1. снова вызывает компонент
  2. создаёт новый Virtual DOM
  3. сравнивает его со старым
  4. обновляет только изменившиеся части DOM

Пример:

function Counter() {
  const [count, setCount] = useState(0)

  console.log("render")

  return (
    <button onClick={() => setCount(count + 1)}>
      {count}
    </button>
  )
}

Каждый клик вызывает новый рендер.

Но DOM обновляется только там, где есть изменения.

Несколько состояний

Компонент может иметь несколько состояний.

function User() {
  const [name, setName] = useState("Anna")
  const [age, setAge] = useState(25)

  return (
    <div>
      <p>{name}</p>
      <p>{age}</p>
    </div>
  )
}

Каждое состояние работает независимо.

Обработка событий

React использует обработчики событий, похожие на обычный JavaScript.

Но они пишутся в camelCase.

HTML:

onclick

React:

onClick

Пример:

function Button() {
  function handleClick() {
    alert("Clicked")
  }

  return <button onClick={handleClick}>Click</button>
}

Можно использовать и стрелочную функцию:

<button onClick={() => console.log("clicked")}>
  Click
</button>

Управляемые поля ввода

React часто управляет формами через состояние.

Пример:

function InputExample() {
  const [text, setText] = useState("")

  return (
    <div>
      <input
        value={text}
        onChange={e => setText(e.target.value)}
      />

      <p>{text}</p>
    </div>
  )
}

Теперь значение поля всегда синхронизировано с состоянием.

Подъём состояния (Lifting State Up)

Иногда несколько компонентов должны использовать одно состояние.

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

Пример:

App
 ├─ Counter
 └─ Counter

Состояние хранится в App и передаётся через props.

Почему React придумал Hooks

Когда React только появился, компоненты писались так:

class User extends React.Component {
  state = { name: "Anna" }

  componentDidMount() {
    console.log("mounted")
  }

  render() {
    return <h1>{this.state.name}</h1>
  }
}

Такие компоненты назывались классовыми.

Но у них было несколько проблем:

  • код быстро становился огромным
  • логика разбрасывалась по разным методам
  • сложно переиспользовать поведение

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

  • componentDidMount
  • componentDidUpdate
  • componentWillUnmount

Код получался запутанным.

React решил проблему радикально.

Вместо классов — обычные функции.

Но функциям нужна была возможность:

  • хранить состояние
  • реагировать на изменения
  • выполнять эффекты

Так появились Hooks.

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

Hook — это просто специальная функция React.

Она даёт компоненту дополнительные возможности.

Например:

  • useState → добавить состояние
  • useEffect → выполнять действия после рендера
  • useRef → хранить ссылку

Компонент при этом остаётся обычной функцией.

function Counter() {
  // hooks

  return <button>Click</button>
}

И это делает код намного проще.

useState — напоминание

Самый первый хук, который изучают — useState.

Он даёт компоненту память.

Без него компонент забывает всё после каждого рендера.

function Counter() {
  let count = 0

  return (
    <button onClick={() => count++}>
      {count}
    </button>
  )
}

Этот код не работает.

Почему?

Потому что после рендера React снова вызывает функцию, и count снова становится 0.

useState решает проблему.

const [count, setCount] = useState(0)

Теперь React запоминает значение между рендерами.

useEffect — когда нужно что-то сделать

Компоненты React должны быть чистыми функциями.

То есть они должны только возвращать интерфейс.

Но иногда нужно сделать что-то ещё.

Например:

  • загрузить данные
  • запустить таймер
  • изменить title страницы
  • подписаться на события

Такие действия называются: Side Effects.

Для них используется useEffect.

Представим реальную ситуацию

Допустим, мы хотим:

когда компонент появился → сделать запрос к API

С useEffect это выглядит так:

useEffect(() => {
  console.log("component mounted")
}, [])

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

То есть когда компонент появился на странице.

Пример из реального приложения

Представим страницу пользователей.

function Users() {

  const [users, setUsers] = useState([])

  useEffect(() => {
    fetch("/api/users")
      .then(r => r.json())
      .then(data => setUsers(data))
  }, [])

  return (
    <ul>
      {users.map(u => (
        <li key={u.id}>{u.name}</li>
      ))}
    </ul>
  )
}

Что происходит:

  1. React рендерит страницу
  2. useEffect делает запрос
  3. данные приходят
  4. состояние обновляется
  5. React делает новый рендер

И список появляется на странице.

Когда useEffect запускается

У useEffect есть три режима работы.

1 — каждый рендер

useEffect(() => {
  console.log("render")
})

Этот эффект будет запускаться всегда.

Обычно так не делают.

2 — только один раз

useEffect(() => {
  console.log("mounted")
}, [])

Это самый частый вариант.

3 — при изменении значения

useEffect(() => {
  console.log("count changed")
}, [count])

Теперь эффект выполняется:

  • при первом рендере
  • и когда изменится count

Важный момент: порядок работы

React работает примерно так:

  1. рендер компонента
  2. обновление DOM
  3. запуск useEffect

То есть эффект выполняется после обновления интерфейса.

Это сделано специально, чтобы рендер был быстрым.

Очистка эффекта

Иногда эффект нужно остановить.

Например:

  • таймер
  • подписка
  • WebSocket
  • event listener

Для этого эффект может вернуть функцию.

useEffect(() => {

  const id = setInterval(() => {
    console.log("tick")
  }, 1000)

  return () => {
    clearInterval(id)
  }

}, [])

React вызовет эту функцию:

  • перед удалением компонента
  • или перед повторным запуском эффекта

Это предотвращает утечки памяти.

useRef — карман для хранения

Иногда нужно хранить значение, которое:

  • не вызывает заново рендер
  • но должно сохраняться

Для этого есть useRef.

const ref = useRef(0)

Теперь можно писать:

ref.current++

И значение сохранится между рендерами.

Но React не будет перерисовывать компонент.

useRef для доступа к DOM

Также useRef используют для доступа к DOM.

Например:

фокус на input

function Search() {

  const inputRef = useRef(null)

  function focusInput() {
    inputRef.current.focus()
  }

  return (
    <div>
      <input ref={inputRef} />
      <button onClick={focusInput}>Фокус</button>
    </div>
  )
}

Hooks: главное, что нужно понять

Hooks нужны для того, чтобы функциональные компоненты могли:

  • хранить состояние
  • выполнять действия после рендера
  • запоминать значения между рендерами

Самые важные хуки в начале:

  • useState
  • useEffect
  • useRef

Именно их реально нужно понять в первую очередь.