JS_Core_React_Core

React Core

React — библиотека JavaScript для создания пользовательских интерфейсов из компонентов. Компонент получает входные данные, может хранить состояние и возвращает описание интерфейса.

function Greeting() {
  return <h1>Привет, React!</h1>;
}

export default function App() {
  return <Greeting />;
}

Компоненты называют с заглавной буквы. Имя со строчной буквы React воспринимает как встроенный HTML-элемент.


JSX

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

const name = 'Анна';
const element = <h1>Привет, {name}!</h1>;

В фигурных скобках размещаются JavaScript-выражения:

<p>{user.name}</p>
<p>{isOnline ? 'В сети' : 'Не в сети'}</p>
<p>Товаров: {items.length}</p>

Инструкции вроде if нельзя вставлять непосредственно в {}. Условие вычисляют заранее либо используют тернарный оператор.

Один корневой элемент

Компонент возвращает один корневой элемент. Если лишний HTML-контейнер не нужен, используется фрагмент:

function UserInfo() {
  return (
    <>
      <h2>Анна</h2>
      <p>Frontend-разработчик</p>
    </>
  );
}

Отличия от HTML

HTML JSX
class className
for htmlFor
tabindex tabIndex
onclick onClick

Все теги закрываются:

<img src={avatarUrl} alt="Аватар" />
<input type="text" />

Строковые атрибуты указываются в кавычках, остальные значения — в фигурных скобках:

<button type="button" disabled={isSaving}>
  Сохранить
</button>

Инлайн-стили передаются объектом, а свойства пишутся в camelCase:

<p style={{ color: 'blue', fontSize: '18px' }}>Текст</p>

React экранирует строки, вставленные через JSX. Необработанный пользовательский HTML нельзя передавать в dangerouslySetInnerHTML: это может привести к XSS.


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

Функциональный компонент — JavaScript-функция, возвращающая JSX:

function Button() {
  return <button type="button">Нажать</button>;
}

const Logo = () => <strong>My App</strong>;

Интерфейс разбивают на небольшие компоненты:

function Header() {
  return <header>Панель управления</header>;
}

function Dashboard() {
  return (
    <div>
      <Header />
      <main>Основное содержимое</main>
    </div>
  );
}

Рендеринг должен оставаться чистым: в теле компонента не следует выполнять запросы, менять DOM или модифицировать внешние переменные. Побочные эффекты выполняются в обработчиках событий или useEffect.


Props

Props — входные данные, которые родитель передаёт дочернему компоненту:

function UserCard({ name, role = 'Пользователь' }) {
  return (
    <article>
      <h2>{name}</h2>
      <p>{role}</p>
    </article>
  );
}

function App() {
  return <UserCard name="Анна" role="Редактор" />;
}

Props могут содержать строки, числа, логические значения, массивы, объекты, функции и React-элементы. Компонент не должен изменять полученные props.

children

Содержимое между тегами компонента доступно через prop children:

function Panel({ title, children }) {
  return (
    <section>
      <h2>{title}</h2>
      <div>{children}</div>
    </section>
  );
}

<Panel title="Профиль">
  <p>Имя: Анна</p>
</Panel>

State и useState

State — внутренние изменяемые данные компонента. Обновление состояния запускает повторный рендеринг.

import { useState } from 'react';

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

  return (
    <button type="button" onClick={() => setCount(count + 1)}>
      Нажатий: {count}
    </button>
  );
}

useState(initialValue) возвращает текущее значение и setter-функцию.

Характеристика Props State
Источник Родитель Сам компонент
Изменение Только родителем Через setter
Назначение Входные данные Изменяемые данные UI

Функциональное обновление

Если новое значение зависит от предыдущего, используется функция:

setCount((previousCount) => previousCount + 1);

Это корректно работает и при нескольких обновлениях:

setCount((value) => value + 1);
setCount((value) => value + 1);
setCount((value) => value + 1);

Состояние конкретного рендера является снимком. Поэтому сразу после setCount() переменная count всё ещё содержит значение текущего рендера.

Объекты и массивы

State считается иммутабельным. Вместо изменения существующего значения создаётся новое:

const [user, setUser] = useState({ name: 'Анна', city: 'Москва' });

setUser((currentUser) => ({
  ...currentUser,
  city: 'Казань',
}));

Операции с массивом:

// Добавление
setTasks((tasks) => [...tasks, newTask]);

// Удаление
setTasks((tasks) => tasks.filter((task) => task.id !== taskId));

// Изменение
setTasks((tasks) =>
  tasks.map((task) =>
    task.id === taskId
      ? { ...task, completed: !task.completed }
      : task
  )
);

Ленивая инициализация

Для дорогого начального вычисления в useState передаётся функция:

const [settings, setSettings] = useState(() => {
  const saved = localStorage.getItem('settings');
  return saved ? JSON.parse(saved) : { theme: 'light' };
});

Производные данные

Данные, полностью вычисляемые из props или state, обычно не нужно хранить отдельно:

const total = items.reduce(
  (sum, item) => sum + item.price * item.quantity,
  0
);

Иначе связанные значения могут рассинхронизироваться.

Подъём состояния

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


Правила хуков

Хуки вызываются:

  1. только на верхнем уровне компонента или пользовательского хука;
  2. не внутри условий, циклов и вложенных функций;
  3. только из React-компонентов и пользовательских хуков.
function Profile() {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  // ...
}

React связывает состояние с порядком вызовов хуков, поэтому порядок должен оставаться одинаковым при каждом рендере.


useEffect

useEffect синхронизирует компонент с внешними системами: браузерными API, сетью, таймерами, подписками и сторонними библиотеками.

import { useEffect } from 'react';

function PageTitle({ title }) {
  useEffect(() => {
    document.title = title;
  }, [title]);

  return <h1>{title}</h1>;
}

Общая форма:

useEffect(() => {
  // Настройка эффекта.

  return () => {
    // Очистка эффекта.
  };
}, [dependencies]);

Зависимости

Без массива эффект запускается после каждого завершённого рендера:

useEffect(() => {
  console.log('После каждого рендера');
});

С пустым массивом эффект настраивается при появлении компонента:

useEffect(() => {
  console.log('Подключение');
  return () => console.log('Отключение');
}, []);

С зависимостями эффект повторно синхронизируется при их изменении:

useEffect(() => {
  document.title = `Поиск: ${query}`;
}, [query]);

Все реактивные значения, прочитанные внутри эффекта, указываются в зависимостях. Это проверяет правило линтера react-hooks/exhaustive-deps.

Очистка

Очистка отменяет таймеры и подписки:

useEffect(() => {
  const id = setInterval(() => {
    setNow(new Date());
  }, 1000);

  return () => clearInterval(id);
}, []);

Пример с обработчиком окна:

useEffect(() => {
  function handleResize() {
    setWidth(window.innerWidth);
  }

  window.addEventListener('resize', handleResize);
  return () => window.removeEventListener('resize', handleResize);
}, []);

Очистка выполняется перед повторным запуском изменившегося эффекта и при удалении компонента.

Загрузка данных

useEffect(() => {
  const controller = new AbortController();

  async function loadUser() {
    try {
      setLoading(true);
      setError(null);

      const response = await fetch(`/api/users/${userId}`, {
        signal: controller.signal,
      });

      if (!response.ok) {
        throw new Error(`HTTP error: ${response.status}`);
      }

      setUser(await response.json());
    } catch (error) {
      if (error.name !== 'AbortError') setError(error);
    } finally {
      if (!controller.signal.aborted) setLoading(false);
    }
  }

  loadUser();
  return () => controller.abort();
}, [userId]);

Callback самого useEffect не делают async: он должен вернуть функцию очистки или undefined, тогда как async-функция возвращает Promise.

Когда эффект не нужен

Обычные вычисления выполняются при рендеринге:

const fullName = `${firstName} ${lastName}`;

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

В режиме разработки StrictMode может выполнить дополнительный цикл настройки и очистки эффекта. Корректно написанная очистка делает такое поведение безопасным.


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

Для выбора JSX используются обычные конструкции JavaScript.

if

function Status({ isLoggedIn }) {
  if (!isLoggedIn) return <LoginForm />;
  return <Dashboard />;
}

Тернарный оператор

<p>{isAdmin ? 'Администратор' : 'Пользователь'}</p>

Логическое &&

{count > 0 && <strong>Уведомлений: {count}</strong>}

Выражение 0 && <Component /> отобразит 0, поэтому число лучше сравнивать явно.

Чтобы ничего не отображать, компонент может вернуть null.


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

Список формируется методом map():

function ProductList({ products }) {
  return (
    <ul>
      {products.map((product) => (
        <li key={product.id}>
          {product.name} — {product.price} ₽
        </li>
      ))}
    </ul>
  );
}

key помогает React сопоставлять элементы между рендерами. Ключ должен быть уникальным среди соседей и стабильным.

<TaskItem key={task.id} task={task} />

Не следует создавать ключ через Math.random() или crypto.randomUUID() во время рендера. Индекс массива подходит только для статического списка, порядок и состав которого не меняются.

key не передаётся компоненту как prop. Если идентификатор нужен внутри, его передают отдельно:

<TaskItem key={task.id} taskId={task.id} task={task} />

Перед сортировкой props-массива нужно создать копию, поскольку sort() мутирует массив:

const sorted = [...products].sort((a, b) => a.price - b.price);

События в React

Обработчик передаётся как функция:

function Button() {
  function handleClick() {
    console.log('Кнопка нажата');
  }

  return <button onClick={handleClick}>Нажать</button>;
}

Не нужно вызывать функцию во время рендера:

<button onClick={handleClick}>Верно</button>
// <button onClick={handleClick()}>Неверно</button>

Для передачи аргумента используется обёртка:

<button onClick={() => onDelete(product.id)}>Удалить</button>

React передаёт объект события:

function handleChange(event) {
  console.log(event.target.value);
}

Часто применяются:

Свойство или метод Назначение
event.target Элемент, где возникло событие
event.currentTarget Элемент с обработчиком
event.preventDefault() Отмена стандартного действия
event.stopPropagation() Остановка распространения

Отправка формы:

function handleSubmit(event) {
  event.preventDefault();
  saveData();
}

<form onSubmit={handleSubmit}>...</form>

Возврат false не отменяет действие браузера: нужен preventDefault().

Обработчики часто передаются дочерним компонентам через props:

function SaveButton({ onSave }) {
  return <button onClick={onSave}>Сохранить</button>;
}

Формы и controlled inputs

В controlled input текущее значение хранится в state:

function NameForm() {
  const [name, setName] = useState('');

  function handleSubmit(event) {
    event.preventDefault();
    console.log({ name });
  }

  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor="name">Имя</label>
      <input
        id="name"
        value={name}
        onChange={(event) => setName(event.target.value)}
      />
      <button type="submit">Отправить</button>
    </form>
  );
}

Связка value и onChange делает React источником истины. Поле с value, но без onChange, доступно только для чтения.

Несколько полей

const [form, setForm] = useState({
  name: '',
  email: '',
  subscribed: false,
});

function handleChange(event) {
  const { name, type, value, checked } = event.target;

  setForm((currentForm) => ({
    ...currentForm,
    [name]: type === 'checkbox' ? checked : value,
  }));
}
<input name="name" value={form.name} onChange={handleChange} />
<input name="email" value={form.email} onChange={handleChange} />
<input
  name="subscribed"
  type="checkbox"
  checked={form.subscribed}
  onChange={handleChange}
/>

Для checkbox используется checked, для textarea и selectvalue. Значение input type="number" также поступает строкой; преобразование выполняют отдельно через Number().

Простая проверка перед отправкой:

function handleSubmit(event) {
  event.preventDefault();

  const email = form.email.trim();

  if (!email || !email.includes('@')) {
    setError('Введите корректный email');
    return;
  }

  setError('');
  sendForm({ ...form, email });
}

Клиентская валидация не заменяет серверную. Сервер должен повторно проверять входные данные.

Файловое поле обычно остаётся неуправляемым:

function handleSubmit(event) {
  event.preventDefault();
  const file = event.currentTarget.elements.avatar.files[0];
}

<form onSubmit={handleSubmit}>
  <input name="avatar" type="file" />
</form>

Жизненный цикл через хуки

Условно выделяют три этапа:

  1. Mount — компонент добавлен в интерфейс.
  2. Update — props или state изменились.
  3. Unmount — компонент удалён.
// Настройка после появления компонента.
useEffect(() => {
  const connection = connect();

  // Очистка перед удалением.
  return () => connection.disconnect();
}, []);

Эффект с зависимостью повторно синхронизируется при её изменении:

useEffect(() => {
  const connection = connectToRoom(roomId);
  return () => connection.disconnect();
}, [roomId]);

Перед новой настройкой React вызывает очистку предыдущего эффекта.

Упрощённая последовательность обновления:

  1. React вызывает компонент и вычисляет JSX — render.
  2. React применяет изменения к DOM — commit.
  3. Браузер отображает результат.
  4. React запускает useEffect.

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


Общий пример: список задач

import { useEffect, useState } from 'react';

function TaskForm({ onAdd }) {
  const [title, setTitle] = useState('');

  function handleSubmit(event) {
    event.preventDefault();
    const normalized = title.trim();
    if (!normalized) return;
    onAdd(normalized);
    setTitle('');
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={title}
        onChange={(event) => setTitle(event.target.value)}
        placeholder="Новая задача"
      />
      <button type="submit">Добавить</button>
    </form>
  );
}

function TaskList({ tasks, onToggle, onDelete }) {
  if (tasks.length === 0) return <p>Задач нет</p>;

  return (
    <ul>
      {tasks.map((task) => (
        <li key={task.id}>
          <label>
            <input
              type="checkbox"
              checked={task.completed}
              onChange={() => onToggle(task.id)}
            />
            {task.title}
          </label>
          <button onClick={() => onDelete(task.id)}>Удалить</button>
        </li>
      ))}
    </ul>
  );
}

export default function App() {
  const [tasks, setTasks] = useState(() => {
    const saved = localStorage.getItem('tasks');
    return saved ? JSON.parse(saved) : [];
  });

  useEffect(() => {
    localStorage.setItem('tasks', JSON.stringify(tasks));
  }, [tasks]);

  function addTask(title) {
    setTasks((current) => [
      ...current,
      { id: crypto.randomUUID(), title, completed: false },
    ]);
  }

  function toggleTask(id) {
    setTasks((current) =>
      current.map((task) =>
        task.id === id
          ? { ...task, completed: !task.completed }
          : task
      )
    );
  }

  function deleteTask(id) {
    setTasks((current) => current.filter((task) => task.id !== id));
  }

  return (
    <main>
      <h1>Задачи</h1>
      <TaskForm onAdd={addTask} />
      <TaskList
        tasks={tasks}
        onToggle={toggleTask}
        onDelete={deleteTask}
      />
    </main>
  );
}

Частые ошибки

Прямая мутация state

// Плохо:
tasks.push(newTask);
setTasks(tasks);

// Хорошо:
setTasks((tasks) => [...tasks, newTask]);

Вызов обработчика при рендере

// Плохо: onClick={deleteTask(task.id)}
// Хорошо:
onClick={() => deleteTask(task.id)}

Нестабильные ключи

// Плохо: key={Math.random()}
// Хорошо: key={task.id}

Бесконечный эффект

useEffect(() => {
  setCount(count + 1);
}, [count]);

Эффект изменяет свою зависимость и запускается снова. Обычно следует пересмотреть необходимость эффекта.

Лишнее состояние

Не храните отдельно отфильтрованный список или итоговую сумму, если их можно вычислить из существующих props и state.

Отсутствие очистки

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


Краткая памятка