Перейти к содержанию
Educora
Продвинутый24 мин25 / 27

Основы React

Собирай интерфейс из компонентов: JSX, props, состояние через `useState`, побочные эффекты через `useEffect` и списки с ключами — ключевые идеи самой популярной UI-библиотеки.

Проверь себя
В этом уроке ты узнаешь
  • Объяснять, что такое компоненты и JSX и как JSX превращается в вызовы функций
  • Передавать данные через props и хранить изменяющиеся данные в useState, обновляя их без изменения исходных
  • Загружать данные через useEffect (зависимости и очистка) и выводить списки со стабильными key

Вспомни список дел из урока про DOM: найти элемент, создать li, добавить его, переключить класс... Когда приложение растёт, вручную держать страницу в согласии с данными становится хаосом. React, созданный в Facebook и открытый в 2013 году, переворачивает подход: ты описываешь, как должен выглядеть интерфейс для текущих данных, а DOM React обновляет сам. Коротко: UI = f(состояние).

Компоненты и JSX

Компонент — функция, которая возвращает описание интерфейса; его имя начинается с заглавной буквы. JSX — похожая на HTML запись внутри JavaScript, а в фигурных скобках { } можно писать любое выражение JavaScript. В JSX вместо class пишут className, а для событий — имена в camelCase, например onClick. Компонент возвращает один корневой элемент (или фрагмент <>...</>).

JavaScript
function Greeting({ name, city }) {
  const hour = 9;
  return (
    <div className="card">
      <h2>Hello, {name}!</h2>
      <p>{city ? `You are in ${city}` : 'Where are you?'}</p>
      <p>{hour < 12 ? 'Good morning' : 'Good afternoon'}</p>
    </div>
  );
}

export default function App() {
  return (
    <>
      <Greeting name="Aysel" city="Baku" />
      <Greeting name="Murad" />
    </>
  );
}
На экране появятся две карточки: «Hello, Aysel! You are in Baku» и «Hello, Murad! Where are you?». Браузер не понимает JSX напрямую — его превращает в обычный JavaScript инструмент сборки, например Vite.

JSX — это просто вызовы функций: <h2 className="title">Hi</h2> превращается в вызов, который создаёт объект с типом элемента, props и дочерними элементами. Повторим это сами с маленькой функцией h — то, что в React называют «виртуальным DOM», и есть дерево таких объектов.

JavaScript
function h(type, props, ...children) {
  return { type, props: props ?? {}, children: children.flat() };
}

function render(node) {
  if (typeof node === 'string' || typeof node === 'number') return String(node);
  if (typeof node.type === 'function') return render(node.type(node.props));
  const attrs = Object.entries(node.props)
    .map(([key, value]) => ` ${key === 'className' ? 'class' : key}="${value}"`)
    .join('');
  return `<${node.type}${attrs}>${node.children.map(render).join('')}</${node.type}>`;
}

const Greeting = ({ name }) => h('h2', { className: 'title' }, 'Hello, ', name, '!');

const app = h('div', null, h(Greeting, { name: 'Aysel' }), h('p', null, 'Items: ', 3));
console.log(JSON.stringify(app.children[1]));
console.log(render(app));
▸ Ожидаемый результат
{"type":"p","props":{},"children":["Items: ",3]}
<div><h2 class="title">Hello, Aysel!</h2><p>Items: 3</p></div>
React сравнивает это дерево с предыдущим и меняет в DOM только различия — это называется согласованием (reconciliation).

Props

Props — входные данные компонента, их передают как атрибуты HTML: строки в кавычках, всё остальное — числа, массивы, объекты, функции — в фигурных скобках. Props только для чтения: компонент не должен их менять. Данные текут сверху вниз (родитель → ребёнок), а события поднимаются вверх через колбэки, переданные в props. Содержимое между тегами попадает в prop children.

JavaScript
function ProductCard({ title, price, inStock = true, onBuy }) {
  return (
    <article className="product">
      <h3>{title}</h3>
      <p>{price.toFixed(2)} AZN</p>
      {inStock ? (
        <button onClick={() => onBuy(title)}>Buy</button>
      ) : (
        <span>Out of stock</span>
      )}
    </article>
  );
}

// inside a parent component:
<ProductCard title="Notebook" price={4.5} onBuy={(name) => console.log('Buying', name)} />
<ProductCard title="Pen" price={1} inStock={false} />
У первой карточки есть кнопка «Buy» — по нажатию в консоли появится Buying Notebook. На второй карточке написано «Out of stock».

Состояние через useState

Состояние (state) — данные, которые меняются со временем и должны обновлять интерфейс: счётчик, введённый текст, список задач. const [value, setValue] = useState(начальное) даёт текущее значение и функцию для его изменения. При вызове setValue React снова вызывает функцию компонента с новым значением (повторный рендер). Прямое присваивание вроде count = count + 1 экран не обновляет. Хуки вызываются только на верхнем уровне компонента — не внутри if или цикла.

JavaScript
import { useState } from 'react';

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

  return (
    <div>
      <p>Clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <button onClick={() => setCount((c) => c + 3)}>+3</button>
      <button onClick={() => setCount(0)}>Reset</button>
    </div>
  );
}
JavaScript
let todos = [
  { id: 1, text: 'Learn JSX', done: true },
  { id: 2, text: 'Learn useState', done: false },
];

const addTodo = (list, text) => [...list, { id: list.length + 1, text, done: false }];
const toggleTodo = (list, id) =>
  list.map((todo) => (todo.id === id ? { ...todo, done: !todo.done } : todo));
const removeTodo = (list, id) => list.filter((todo) => todo.id !== id);

const before = todos;
todos = addTodo(todos, 'Learn useEffect');
todos = toggleTodo(todos, 2);
todos = removeTodo(todos, 1);

console.log(todos.map((t) => `${t.done ? '[x]' : '[ ]'} ${t.text}`));
console.log(before === todos, before.length);
▸ Ожидаемый результат
[ '[x] Learn useState', '[ ] Learn useEffect' ]
false 2
В компоненте React эти функции вызывают так: setTodos((list) => addTodo(list, text)). Старый массив остался нетронутым — React видит новую ссылку и обновляет экран.

Эффекты через useEffect

Рендер должен быть чистым: он лишь вычисляет интерфейс из props и состояния. Работа, затрагивающая внешний мир, — загрузка данных, таймеры, подписки — это побочные эффекты, их пишут в useEffect(fn, зависимости). React запускает эффект после того, как рендер попал на экран. Массив зависимостей управляет моментом: [] — один раз после первого рендера; [userId] — при каждом изменении userId; без массива — после каждого рендера. Функция, возвращённая из эффекта, — очистка: она отменяет таймеры и запросы, когда компонент исчезает или перед повторным запуском эффекта.

JavaScript
import { useEffect, useState } from 'react';

function UserCard({ userId }) {
  const [user, setUser] = useState(null);
  const [error, setError] = useState('');

  useEffect(() => {
    const controller = new AbortController();
    setUser(null);
    fetch(`https://api.example.com/users/${userId}`, { signal: controller.signal })
      .then((res) => {
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        return res.json();
      })
      .then(setUser)
      .catch((err) => {
        if (err.name !== 'AbortError') setError(err.message);
      });
    return () => controller.abort();
  }, [userId]);

  if (error) return <p>Error: {error}</p>;
  if (!user) return <p>Loading...</p>;
  return <h2>{user.name}</h2>;
}
Когда userId меняется, очистка отменяет старый запрос — запоздавший ответ для прежнего пользователя не перезапишет нового.

Списки и ключи

Массив выводят через map: для каждого элемента возвращается JSX, и каждому нужен key — идентификатор, уникальный среди соседей и стабильный (обычно id из данных). Благодаря ключам React узнаёт элементы между рендерами: когда что-то добавляется, удаляется или перемещается, он знает, какой узел DOM принадлежит какому элементу. Если список может меняться, не используй индекс как ключ.

JavaScript
function TodoList({ todos, onToggle }) {
  if (todos.length === 0) return <p>Nothing to do!</p>;

  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id} className={todo.done ? 'done' : ''} onClick={() => onToggle(todo.id)}>
          {todo.text}
        </li>
      ))}
    </ul>
  );
}
  1. 1
    Создай проект

    Выполни в терминале: npm create vite@latest my-app -- --template react

  2. 2
    Установи зависимости

    cd my-app и npm install

  3. 3
    Запусти

    npm run dev, затем открой в браузере http://localhost:5173. Страница сразу обновляется, когда ты меняешь src/App.jsx.

Задание

Хук useReducer работает именно с такими редьюсерами. Допиши cartReducer(cart, action): add добавляет новый товар с qty: 1, increment увеличивает количество товара на 1, remove удаляет товар. Не изменяй ни один массив или объект — всегда возвращай новый.

Задание · JavaScript
function cartReducer(cart, action) {
  switch (action.type) {
    case 'add':
      // return a new array with { id: action.id, qty: 1 } at the end
    case 'increment':
      // return a new array where that item has qty + 1
    case 'remove':
      // return a new array without that item
    default:
      return cart;
  }
}

const start = [{ id: 'tea', qty: 1 }];
const actions = [
  { type: 'add', id: 'bread' },
  { type: 'increment', id: 'tea' },
  { type: 'increment', id: 'tea' },
  { type: 'remove', id: 'bread' },
];

console.log(actions.reduce(cartReducer, start));
console.log(start);
▸ Ожидаемый результат
[ { id: 'tea', qty: 3 } ]
[ { id: 'tea', qty: 1 } ]
Задание

Даны функции h и render из урока. С помощью h напиши компонент TodoList({ todos }): он возвращает ul, в котором на каждую задачу приходится один li, а li выполненной задачи получает className: 'done'.

Задание · JavaScript
function h(type, props, ...children) {
  return { type, props: props ?? {}, children: children.flat() };
}

function render(node) {
  if (typeof node === 'string' || typeof node === 'number') return String(node);
  if (typeof node.type === 'function') return render(node.type(node.props));
  const attrs = Object.entries(node.props)
    .map(([key, value]) => ` ${key === 'className' ? 'class' : key}="${value}"`)
    .join('');
  return `<${node.type}${attrs}>${node.children.map(render).join('')}</${node.type}>`;
}

// map todos to li elements inside the ul
const TodoList = ({ todos }) => h('ul', null);

const todos = [
  { id: 1, text: 'Learn JSX', done: true },
  { id: 2, text: 'Learn hooks', done: false },
];
console.log(render(h(TodoList, { todos })));
▸ Ожидаемый результат
<ul><li class="done">Learn JSX</li><li>Learn hooks</li></ul>

Главное

  • Компонент — функция, которая по props возвращает интерфейс; JSX компилируется в вызовы функций, описывающие элементы.
  • Props текут сверху вниз и доступны только для чтения; события поднимаются вверх через колбэки в props.
  • useState даёт значение и функцию-сеттер; вызов сеттера запускает повторный рендер; массивы и объекты обновляй без мутаций.
  • useEffect — для побочных эффектов после рендера: массив зависимостей выбирает момент, а возвращённая функция выполняет очистку.
  • Списки выводят через map, и каждому элементу нужен стабильный уникальный key — не индекс, если список может меняться.

Проверь себя

Вопросов: 10. Каждый правильный ответ приносит XP.

1 / 10
Во что превращается JSX после сборки?