Главная / Статьи / От скриптов DOM к интерфейсу как функции состояния: основная модель React

От скриптов DOM к интерфейсу как функции состояния: основная модель React

Почему React заменяет ручные обновления DOM на декларативные компоненты, и как JSX, пропсы, состояние, перерисовка и однонаправленный поток данных вписываются в единую концептуальную модель.

4868 слов

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

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

Проблема, которую решает React

До того, как библиотеки компонентов стали нормой, интерактивный пользовательский интерфейс подразумевал использование императивного кода: необходимо было находить элементы и вручную вносить все изменения каждый раз, когда что-то происходило. Для простой кнопки «Лайк» настройка выглядит так:

// Traditional DOM manipulation
const button = document.getElementById("like-button");
const countEl = document.getElementById("like-count");
const icon = document.getElementById("like-icon");
let liked = false;
let count = 42;

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

button.addEventListener("click", function() {
  if (!liked) {
    liked = true;
    count++;
    countEl.textContent = count;
    button.classList.add("liked");
    button.classList.remove("unliked");
    icon.src = "/icons/heart-filled.svg";
    button.style.color = "#e0245e";
    // trigger animation
    button.classList.add("animate-pop");
    setTimeout(() => button.classList.remove("animate-pop"), 300);
  } else {
    liked = false;
    count--;
    countEl.textContent = count;
    button.classList.remove("liked");
    button.classList.add("unliked");
    icon.src = "/icons/heart-empty.svg";
    button.style.color = "#6c757d";
  }
});

Это ещё управляемо для одной кнопки. Теперь представьте ленту из 50 постов, у каждого из которых есть собственные элементы управления для «Лайк», комментариев, репоста и сохранения; все они могут меняться из-за кликов пользователей или поступления новых данных с сервера. В результате скрипт превращается в сложную сеть поисков элементов, обработчиков событий и переменных, которые необходимо вручную синхронизировать. По мере роста приложения появляются три основных способа сбоя:

  • Синхронизация. DOM показывает одно, а ваши переменные — другое. Вы обновляете элемент-счётчик, но забываете о переменной, или наоборот, и теперь вам нужно отлаживать два источника информации.
  • Повторное использование. Обработчик привязан к конкретным идентификаторам элементов и определённой структуре HTML, поэтому перемещение кнопки на другую страницу означает необходимость её переписывания.
  • Сложность. Каждая новая функция заставляет вас понимать всё остальное, с чем она может взаимодействовать. Небольшие изменения могут нарушить работу отдалённых частей страницы.

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

JSX: маркировка, которая компилируется в JavaScript

Первой необычной особенностью кода React является маркировка, похожая на HTML, внутри функции:

function Greeting() {
  return (
    <div className="greeting">
      <h1>Hello, Priya!</h1>
      <p>Welcome back.</p>
    </div>
  );
}

Эта маркировка — это JSX, расширение синтаксиса, которое позволяет записывать деревья элементов в файлах JavaScript. Это ни HTML, ни что-либо ещё, понятное браузеру. На этапе сборки (Babel, компилятор TypeScript или инструмент для объединения кода, использующий их) она преобразуется в обычные вызовы функций до того, как код будет выполнен. Вы пишете:

// What you write (JSX):
return (
  <h1 className="title">Hello!</h1>
);

а компилятор генерирует что-то эквивалентное:

// What the compiler transforms it into:
return React.createElement("h1", { className: "title" }, "Hello!");

React.createElement просто возвращает обычный объект, описывающий элемент: его тип, атрибуты и дочерние элементы. React анализирует эти объекты, чтобы определить, что должно находиться в реальном DOM. (В более новых версиях JSX вместо этого используется другой вспомогательный инструмент из react/jsx-runtime, но результат — это тот же самый описательный объект.) Никто не пишет такие вызовы вручную; JSX существует потому, что вложенная разметка гораздо легче читается.

В чем JSX отличается от HTML

Синтаксис близок к HTML, но не идентичен. Основные отличия, которые чаще всего создают затруднения:

// HTML                              JSX
// ─────────────────────────────     ──────────────────────────────────
// class="title"                    className="title"  ← JS reserved word
// for="email"                      htmlFor="email"    ← JS reserved word
// <input> (self-closing optional)  <input />          ← must self-close
// onclick="handler()"              onClick={handler}  ← camelCase, no quotes
// style="color: red"               style={{ color: "red" }}  ← JS object

Классы и циклы в JavaScript являются зарезервированными словами, поэтому в JSX используются className и htmlFor. Каждый элемент должен быть закрыт. Обработчики событий пишутся в формате camelCase и принимают функцию, а не строку. Встроенные стили представляют собой объекты. Чтобы узнать больше о компромиссах, лежащих в основе этой синтаксической конструкции, прочитайте почему JSX — это не HTML.

Встраивание выражений JavaScript

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

function UserCard({ user }) {
  const isOnline = user.lastSeen < Date.now() - 5 * 60 * 1000;

Затем несколько выражений формируют структуру маркипада: сокращенное описание, условное имя класса, условный текст метки и форматированная дата:

  return (
    <div className="card">
      <img src={user.avatar} alt={user.name} />
      <h2>{user.name}</h2>
      <p>{user.bio.length > 100 ? user.bio.slice(0, 100) + "..." : user.bio}</p>
      <span className={isOnline ? "badge-green" : "badge-grey"}>
        {isOnline ? "Online" : "Offline"}
      </span>
      <p>Joined: {new Date(user.joinedAt).toLocaleDateString()}</p>
    </div>
  );
}

Правило простое: скобки используются для выражений, а не для операторов. Можно использовать тернарный оператор, но не блок if, а также метод .map(), но не цикл for, прямо внутри маркировки.

Компоненты: функции, возвращающие интерфейс

Компонент — это повторно используемый, самодостаточный элемент интерфейса, написанный в виде функции JavaScript, которая возвращает JSX. Вот и всё определение:

// A component is just a function that returns JSX
function Button() {
  return (
    <button className="btn">
      Click me
    </button>
  );
}

Его используют так же, как HTML-теги, и можно разместить столько, сколько нужно:

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

Это отрисовывает три идентичных кнопки на основе одного определения.

Составление небольших компонентов в более крупные

Компоненты становятся мощными, когда они вложены друг в друга. Небольшие компоненты с одной функцией объединяются в более крупные, подобно строительным блокам. Аватар знает только, как отображать изображение:

function Avatar({ src, alt }) {
  return <img className="avatar" src={src} alt={alt} />;
}

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

function UserName({ name, handle }) {
  return (
    <div>
      <strong>{name}</strong>
      <span>@{handle}</span>
    </div>
  );
}function UserInfo({ user }) {
  return (
    <div className="user-info">
      <Avatar src={user.avatar} alt={user.name} />
      <UserName name={user.name} handle={user.handle} />
    </div>
  );
}function PostCard({ post, author }) {
  return (
    <article className="post-card">
      <UserInfo user={author} />
      <p>{post.content}</p>
      <span>{post.likes} likes</span>
    </article>
  );
}

У каждого слоя есть своя задача. Avatar отображает изображение, UserInfo размещает аватар рядом с именем, PostCard структурирует весь пост. Именно такой подход пропагандируется React: разделять интерфейс на самые маленькие осмысленные части, назначать каждой из них четкую функцию и затем объединять их. руководство по созданию повторно используемых компонентов React подробнее рассматривает дизайн компонентов.

Почему имена компонентов пишутся с заглавной буквы

React различает нативные элементы и компоненты по первой букве. <button> превращается в обычный элемент DOM button; <Button> заставляет React искать в текущем контексте переменную с именем Button и вызывать её. Компонент, названный строчной буквой, тихо считается неизвестным HTML-тегом, что часто становится причиной путаницы вида «мой компонент ничего не отображает».

Props: настройка компонента извне

Кнопка, которая всегда показывает текст «Нажмите меня», не очень полезна. Props (сокращение от properties) — это способ, с помощью которого родительский компонент передаёт данные дочернему, так что одно определение может подходить для многих ситуаций.

Props передаются в одном направлении — от компонента, который отображает другой, к самому отображаемому компоненту, и приходят в виде одного объекта-аргумента. Родительский компонент задаёт их в виде атрибутов:

// Parent passes data as props (looks like HTML attributes)
function App() {
  return (
    <div>
      <Button label="Submit" colour="blue" />
      <Button label="Cancel" colour="grey" />
      <Button label="Delete" colour="red" />
    </div>
  );
}

И дочерний элемент извлекает необходимые ему данные:

// Child receives them as an object
function Button({ label, colour }) {
  return (
    <button className={`btn btn-${colour}`}>
      {label}
    </button>
  );
}

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

Параметры могут содержать любые значения

Строки — это лишь начало. Числа, логические значения, массивы, объекты и функции — все это допустимые параметры. Например, карточка товара может принимать объект с данными, функцию-обратный вызов и флаг:

function ProductCard({ product, onAddToCart, featured }) {
  return (
    <div className={`card ${featured ? "card-featured" : ""}`}>
      <img src={product.image} alt={product.name} />
      <h3>{product.name}</h3>
      <p>₹{product.price.toLocaleString()}</p>
      <p>{product.rating} ★ ({product.reviewCount} reviews)</p>
      <button onClick={onAddToCart}>
        Add to Cart
      </button>
    </div>
  );
}

В месте вызова эти значения передаются с использованием скобок:

// Usage:
<ProductCard
  product={{ name: "Headphones", price: 2499, rating: 4.3, reviewCount: 128 }}
  onAddToCart={() => addToCart(product.id)}
  featured={true}
/>

Обратите внимание, что встроенный обратный вызов onAddToCart ссылается на product.id, но здесь product — это лишь объект, переданный в качестве свойства, а не переменная из текущего области видимости. В реальном коде следует использовать переменную, определённую в родительском компоненте, например onAddToCart={() => addToCart(item.id)}. Передача функций в качестве свойств — это стандартный способ для дочерних компонентов сообщать о событиях, что ещё раз будет рассмотрено ниже.

Свойства являются только для чтения

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

// WRONG — never modify props
function Button({ count }) {
  count = count + 1; // ← this is a mistake
  return <button>{count}</button>;
}

Переопределение переменной изменяет только локальную переменную; оно никогда не доходит до родительского компонента, и она исчезает при следующей отрисовке. Когда компоненту нужна значение, которое может меняться, вот для чего существует состояние:

// CORRECT — props are read-only, use state for data that changes
function Button({ initialCount }) {
  const [count, setCount] = useState(initialCount);
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}

Здесь initialCount лишь инициализирует состояние; после первой отрисовки компонент сам управляет своим счётчиком.

Состояние: собственная память компонента

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

Состояние создаётся с помощью хука useState, импортируемого из React:

import { useState } from "react";

Счётчик демонстрирует базовую структуру:

function Counter() {
  const [count, setCount] = useState(0);
  //     ↑ current value   ↑ function to update it    ↑ initial value  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={() => setCount(count - 1)}>Decrement</button>
      <button onClick={() => setCount(0)}>Reset</button>
    </div>
  );
}

useState(0) возвращает пару: текущее значение (count, которое начинается с 0) и функцию-установщик (setCount). Вызов установщика с новым значением сообщает React сохранить его и перерисовать компонент.

Пересоздание кнопки «Лайк» с использованием состояния

Императивная кнопка «Лайк» из начала становится гораздо меньше по размеру. Начнем с импорта:

import { useState } from "react";

а затем опишем внешний вид кнопки для любой комбинации значений liked и count:

function LikeButton({ initialLikes }) {
  const [liked, setLiked] = useState(false);
  const [count, setCount] = useState(initialLikes);  function handleClick() {
    if (liked) {
      setLiked(false);
      setCount(c => c - 1);
    } else {
      setLiked(true);
      setCount(c => c + 1);
    }
  }  return (
    <button
      onClick={handleClick}
      className={liked ? "btn-liked" : "btn-default"}
    >
      {liked ? "♥" : "♡"} {count}
    </button>
  );
}

Здесь нет поиска элементов, нет вызовов classList и нет присваиваний textContent. Обработчик обновляет два значения состояния, а JSX определяет внешний вид кнопки для этих значений. React приводит DOM в соответствие с этим видом.

Обратите внимание на форму обновления setCount(c => c - 1). Передача функции вместо значения позволяет получить самое последнее состояние, что безопаснее, когда несколько обновлений заданы в очередь в рамках одного события.

Каждый экземпляр сохраняет своё собственное состояние

Состояние принадлежит конкретному экземпляру компонента, а не определению компонента. При отрисовке трёх похожих кнопок у каждой будет своё значение liked и count; нажатие на одну не повлияет на остальные:

function Feed() {
  return (
    <div>
      <LikeButton initialLikes={24} />   {/* has its own state */}
      <LikeButton initialLikes={7} />    {/* has its own state */}
      <LikeButton initialLikes={156} />  {/* has its own state */}
    </div>
  );
}

Что должно находиться в состоянии

Используйте состояние для значений, которые:

  • меняются со временем из-за взаимодействия или поступления данных
  • должны обновлять интерфейс при своих изменениях
  • принадлежат именно этому конкретному экземпляру компонента

Избегайте хранения в состоянии того, что можно вычислить из существующих свойств или состояния (лучше вычисляйте это во время отрисовки), а также значений, которые меняются без необходимости повторной отрисовки, таких как идентификатор таймера; им лучше соответствовать использование ref.

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

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

При первой отрисовке счётчик генерирует свою разметку, и React создаёт узлы DOM:

Initial render:
  count = 0
  Component runs → returns <p>Count: 0</p> <button>Increment</button>
  React creates DOM nodes

Когда пользователь нажимает, функция-установщик запускает новую отрисовку, и React сравнивает два описания:

User clicks Increment:
  setCount(1) called
  React re-renders the component
  count = 1
  Component runs again → returns <p>Count: 1</p> <button>Increment</button>
  React compares: <p>Count: 0</p> vs <p>Count: 1</p>
  React updates only the text node inside <p>
  Button is unchanged — React leaves it alone

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

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

Компонент снова рендерится, когда меняется его собственное состояние:

1. The component's own state changes (setCount, setUser, etc.)
         ↓
   Component re-renders

Он также снова рендерится в нескольких других ситуациях:

2. The component's props change (parent passes different values)
         ↓
   Component re-renders3. A context the component uses changes
         ↓
   Component re-renders4. The parent re-renders
         ↓
   All children re-render (unless memoized)

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

Такая каскадная обработка редко становится проблемой. Отрисовка — это всего лишь вызов функции, создающий объекты; дорогостоящей частью является взаимодействие с реальным DOM, которое React старается свести к минимуму. Большинство настоящих проблем с производительностью возникают из-за структуры компонентов и состояния, а не из-за простого количества отрисовок.

Виртуальный DOM и согласование

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

Вы никогда не работаете напрямую с этим слоем; это деталь реализации. Это важно потому, что сравнение обычных объектов в памяти происходит быстро, тогда как реальные операции с DOM относительно медленны. Перенаправление каждой обновления через такое сравнение позволяет React группировать операции и минимизировать затратные действия.

Декларативный и императивный интерфейс

React является декларативным, и понимание этого термина — ключевой момент для правильного восприятия его принципов. Сравните два способа отображения списка.

Императивный подход: перечисление шагов

В императивной версии сначала опустошается контейнер:

// Imperative: manual DOM manipulation
const list = document.getElementById("list");
list.innerHTML = "";  // clear it

затем вручную создаются, настраиваются и добавляются все элементы:

items.forEach(item => {
  const li = document.createElement("li");
  li.textContent = item.name;
  li.className = item.active ? "active" : "";
  li.addEventListener("click", () => handleClick(item.id));
  list.appendChild(li);
});

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

Декларативный подход: описание результата

Декларативная версия описывает, каким должен быть список для любого массива items:

// Declarative: describe the desired output
function ItemList({ items, onItemClick }) {
  return (
    <ul>
      {items.map(item => (
        <li
          key={item.id}
          className={item.active ? "active" : ""}
          onClick={() => onItemClick(item.id)}
        >
          {item.name}
        </li>
      ))}
    </ul>
  );
}

Не существует подхода «очистить список, затем воссоздать его». Вы описываете желаемый результат, а React сам определяет, как добраться от текущего состояния DOM до этого результата. Атрибут key указывает React, какой элемент является каким между перерисовками, что позволяет ему перемещать, обновлять или удалять нужные элементы вместо того, чтобы создавать их заново.

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

  • Прогнозируемость. При одинаковых атрибутах и состоянии компонент отображает одинаковый результат. Интерфейс можно рассматривать как чистую функцию от данных: UI = f(state, props).
  • Меньше информации, которую нужно запоминать. Вам не приходится отслеживать, как выглядит DOM в данный момент или какие изменения ему требуются. Достаточно описать текущее состояние.
  • Меньше багов. Большинство проблем с ручным управлением DOM, таких как обновление одного элемента без изменения соседних элементов или несоответствие обработчиков данным, просто не могут возникнуть, когда вся интерфейсная структура формируется на основе состояния.
  • Дерево компонентов и однонаправленный поток данных

    Каждое приложение React представляет собой дерево. Корневой компонент, обычно App, отрисовывает своих дочерних компонентов, которые в свою очередь отрисовывают своих дочерних компонентов, воспроизводя структуру экрана:

    App
    ├── Navbar
    │   ├── Logo
    │   ├── NavLinks
    │   └── UserMenu
    │       ├── Avatar
    │       └── DropdownMenu
    ├── Dashboard
    │   ├── Sidebar
    │   │   ├── SidebarLink (×5)
    │   │   └── UserStats
    │   └── MainContent
    │       ├── StatsRow
    │       │   ├── StatCard (×4)
    │       └── RecentActivity
    │           ├── ActivityItem (×10)
    └── Footer
    

    Поток данных сверху вниз

    Данные передаются от родителя к дочернему компоненту через атрибуты props. Компонент не может получить доступ к состоянию своего родителя или братьев-компонентов. Именно этот однонаправленный поток делает крупные приложения понятными:

    App (has user, notifications, theme)
      │
      ├── Navbar (receives: user, notifications)
      │     │
      │     └── UserMenu (receives: user)
      │           │
      │           └── Avatar (receives: user.avatar, user.name)
      │
      └── Dashboard (receives: user, theme)
            │
            └── MainContent (receives: theme)
    

    Avatar видит только то, что ему передаёт UserMenu, а UserMenu видит только то, что ему передаёт Navbar. Ничего не утечёт в сторону или наверх, поэтому при ошибочном значении достаточно проследить цепочку родителей.

    Поток событий вверх

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

    // Parent owns the state and passes down both the value and the updater
    function App() {
      const [searchQuery, setSearchQuery] = useState("");
    

    и передаёт как значение, так и функцию-установщик своим дочерним компонентам; поисковая строка вызывает функцию-установщик каждый раз, когда меняется введённый текст:

      return (
        <div>
          <SearchBar
            query={searchQuery}
            onChange={setSearchQuery}   {/* passes the setter as a prop */}
          />
          <Results query={searchQuery} />
        </div>
      );
    }// Child receives the updater and calls it on user input
    function SearchBar({ query, onChange }) {
      return (
        <input
          value={query}
          onChange={e => onChange(e.target.value)}  {/* calls parent's setter */}
          placeholder="Search..."
        />
      );
    }
    

    Запрос существует только в компоненте App. Компонент SearchBar ничего не хранит; он передаёт информацию о каждом нажатии клавиши через свойство onChange. Компонент Results получает обновлённый запрос в виде свойства и перерисовывается. Данные передаются вниз по иерархии в виде свойств, а события — вверх посредством вызовов функций. (Пояснительные комментарии внутри открывающих тегов предназначены только для чтения; в реальном JSX комментарий в квадратных скобках должен находиться среди дочерних элементов, а внутри тега следует использовать обычный комментарий /* */ или вообще его не писать.)

    Ошибки, нарушающие обновление интерфейса в React

    Изменение состояния непосредственно

    Хочется модифицировать объект состояния напрямую и передавать его обратно:

    // WRONG — mutating state directly
    const [user, setUser] = useState({ name: "Priya", age: 28 });
    

    В первом примере с birthday объект изменяется, и в метод setter передаётся тот же самый ссылочный объект, поэтому интерфейс не обновляется. Во втором примере с помощью оператора spread создаётся новый объект, что React воспринимает как изменение:

    function birthday() {
      user.age = 29;        // ← directly modifying the object
      setUser(user);        // ← same object reference — React sees no change
    }                       //   UI does NOT update// CORRECT — create a new object
    function birthday() {
      setUser({ ...user, age: user.age + 1 }); // ← new object, React detects change
    }
    

    React определяет, изменилось ли состояние, сравнивая ссылки с помощью Object.is, а не проверяя их содержимое. Если изменить объект или массив на месте и вернуть ту же ссылку, React придет к выводу, что ничего не произошло, и может пропустить отрисовку.

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

    // WRONG — mutating the array
    const [items, setItems] = useState([1, 2, 3]);
    items.push(4);       // ← modifies in place
    setItems(items);     // ← same reference — no re-render
    

    В то время как копирование в новый массив сработает:

    // CORRECT — create a new array
    setItems([...items, 4]);  // ← spread creates a new array
    

    Путаница между пропсами и состоянием

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

    // Wrong: using state for something that should be a prop
    function UserCard() {
      const [userName] = useState("Priya"); // ← why is this state? It never changes here
      return <p>{userName}</p>;
    }
    

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

    // Correct: static data the parent controls comes as a prop
    function UserCard({ name }) {
      return <p>{name}</p>;
    }
    

    Хранение значений, которые можно вычислить

    Не всё, что меняется, требует собственного состояния. Хранение подсчёта рядом с списком, который он отслеживает, приводит к дублированию информации:

    // Wrong: storing derived data in state
    const [items, setItems] = useState([...]);
    const [itemCount, setItemCount] = useState(0); // ← why is this state?
    

    Теперь любые изменения в items требуют соответствующего обновления itemCount, и одно забытое обновление приводит к их несоответствию. Вычисление этого значения во время отрисовки по своей сути обеспечивает синхронизацию обоих элементов:

    // Every time items changes, you have to remember to also update itemCount
    // And if you forget once, they're out of sync// Correct: derive it during render
    const [items, setItems] = useState([...]);
    const itemCount = items.length; // ← just a variable — always in sync
    

    Компоненты, которые делают всё

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

    // Hard to maintain — does everything
    function UserDashboard() {
      // manages user state
      // fetches orders
      // handles filters
      // manages pagination
      // controls modal open/close
      // renders sidebar
      // renders order table
      // renders filter controls
      // renders pagination
      // renders modal
      return ( /* 200 lines of JSX */ );
    }
    

    Разделение компонента по функциям позволяет каждой его части иметь название и определённую задачу:

    // Better — clear single responsibilities
    function UserDashboard() {
      return (
        <DashboardLayout>
          <OrderFilters />
          <OrderTable />
          <OrderPagination />
          <OrderDetailModal />
        </DashboardLayout>
      );
    }
    

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

    Определите границы до написания кода

    Начните с дизайна и отметьте составные элементы. Все, что повторяется, является компонентом. Все, у чего есть одна четко определенная функция, также являются компонентами. Элементы, которые меняются вместе, должны находиться вместе; элементы, которые меняются независимо, следует разделить. Для страницы каталога продуктов дизайн:

    Looking at the design:
      [ Filter Bar                          ]
      [ Product Card ][ Product Card ][ Product Card ]
      [ Product Card ][ Product Card ][ Product Card ]
      [ Pagination                          ]
    

    разбивается на следующую структуру:

    Components:
      ProductPage
      ├── FilterBar
      │   ├── FilterGroup (×3)
      │   └── SortDropdown
      ├── ProductGrid
      │   └── ProductCard (×N)
      └── Pagination
    

    Размещайте состояние у самого низкого общего родителя

    Для каждого элемента состояния найдите в структуре самый низкий компонент, который его нуждается, и храните состояние там:

    If only ProductCard needs the "expanded" state → put it in ProductCard
    If FilterBar and ProductGrid both need filters → put filters in ProductPage
    If Pagination and ProductGrid both need currentPage → put it in ProductPage
    

    Когда двум соседним элементам нужны одни и те же данные, переместите состояние к их ближайшему общему родителю и передавайте его дальше. Это называется поднятием состояния вверх. Размещение состояния как можно ниже также сокращает объем перерисовки всей структуры при его изменении.

    Сначала создайте статическую версию, затем добавьте состояние

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

    // Step 1: static — no state, hardcoded data
    function ProductCard() {
      return (
        <div className="card">
          <img src="/headphones.jpg" alt="Headphones" />
          <h3>Wireless Headphones</h3>
          <p>₹2,499</p>
          <button>Add to Cart</button>
        </div>
      );
    }
    

    Второй шаг заключается в замене жестко заданного содержимого на атрибут product, а третий шаг — в добавлении небольшого элемента состояния для подтверждения «Добавлено», который сбрасывается сам по себе через две секунды:

    // Step 2: accept props — still no state
    function ProductCard({ product }) {
      return (
        <div className="card">
          <img src={product.image} alt={product.name} />
          <h3>{product.name}</h3>
          <p>₹{product.price.toLocaleString()}</p>
          <button>Add to Cart</button>
        </div>
      );
    }// Step 3: add state for interactivity
    function ProductCard({ product, onAddToCart }) {
      const [added, setAdded] = useState(false);  function handleAdd() {
        setAdded(true);
        onAddToCart(product.id);
        setTimeout(() => setAdded(false), 2000);
      }  return (
        <div className="card">
          <img src={product.image} alt={product.name} />
          <h3>{product.name}</h3>
          <p>₹{product.price.toLocaleString()}</p>
          <button onClick={handleAdd} disabled={added}>
            {added ? "Added ✓" : "Add to Cart"}
          </button>
        </div>
      );
    }
    

    Поскольку структура и интерактивность разделены, каждый шаг легко проверить отдельно.

    Основные выводы

    Вся концепция в кратком изложении. Зачем существует React:

    Why React:
      Plain JS DOM manipulation is hard to scale and maintain
      React: describe the UI, let React handle DOM updates
    

    а также основы каждой из рассмотренных выше концепций, от JSX до распространенных ошибок:

    JSX:
      HTML-like syntax in JavaScript — compiled to React.createElement()
      {} embeds any JavaScript expression
      Use className (not class), onClick (not onclick)Components:
      Functions that return JSX
      Capital letter names (Button, not button)
      Reusable, composable building blocksProps:
      Data passed from parent to child (like function arguments)
      Read-only — a component never modifies its own props
      Can be strings, numbers, objects, arrays, functionsState:
      const [value, setValue] = useState(initialValue)
      Data owned by a component that changes over time
      Calling the setter triggers a re-render
      Each component instance has its own stateRe-rendering:
      Happens when state changes, props change, or parent re-renders
      React diffs the virtual DOM and updates only what changed
      Not expensive — surgical DOM updatesDeclarative:
      Describe what the UI should look like
      React figures out what changed and how to update the DOM
      UI = f(state, props) — predictable, testableData flow:
      Props flow down (parent → child)
      Events flow up (child calls parent's function)
      One-way data flow keeps the application predictableCommon mistakes:
      Mutating state directly (use spread / new objects)
      Storing derived values in state (compute them during render)
      Overloading one component (split by responsibility)
    
    • Компоненты — это функции, пропсы — их аргументы, а состояние — их память. Интерфейс всегда является проекцией текущего состояния.
    • Повторная отрисовка по замыслу реализована дешево; дорогостоящей частью является работа с DOM, которую React за вас выполняет. Сначала правильно организуйте состояние, прежде чем беспокоиться о количестве отрисовок.
    • Всегда передавайте в функции-установщики новые объекты и массивы; React сравнивает ссылки, а не содержимое.
    • Храните состояние в том компоненте, который его наиболее нуждается, вычисляйте всё остальное во время отрисовки и позволяйте событиям передаваться вверх через пропсы-обратные вызовы.

    Контекст, эффекты, пользовательские хуки и настройка производительности основаны на этих принципах. Если у вас хорошее понимание компонентов, пропсов, состояния и повторной отрисовки, более сложные аспекты React становятся расширением уже известной вам модели, а не новыми правилами, которые нужно запоминать.

    Связанная литература

  • Redux Toolkit от First Slice до асинхронных данных: рабочая модель понимания — Узнайте, как хранилище Redux Toolkit, компоненты slices, редьюсеры на основе Immer, селекторы и функция createAsyncThunk взаимодействуют друг с другом, на примере счётчика, корзины покупок и списка товаров.