Вы создали карточку товара и добавили кнопку «В избранное». После нажатия иконка должна измениться, а товар появиться в списке сохранённых. Если использовать обычную переменную JavaScript, React не узнает, что интерфейс нужно обновить. Для таких данных существует состояние компонента. В React его можно создать с помощью useState. Разберём этот механизм на небольшом каталоге товаров.
Чем состояние отличается от обычной переменной
Представим компонент, внутри которого объявлена переменная count. При нажатии кнопки программа увеличивает её на единицу, но React не получает сигнала о необходимости заново отобразить компонент. Кроме того, при следующем рендеринге локальная переменная создаётся заново. Поэтому она не подходит для данных, которые интерфейс должен помнить между обновлениями.
useState решает обе задачи. Он сохраняет значение между рендерами и предоставляет функцию, которая сообщает React об изменении. Выглядит это так:
const [count, setCount] = useState(0);
count содержит текущее значение. setCount используется для его обновления. Число 0 задаёт исходное состояние. Когда вызывается setCount, React запоминает новое значение и планирует следующий рендер компонента.
Делаем список избранных товаров
Возьмём каталог из трёх товаров. Пользователь сможет нажимать кнопку рядом с каждым из них, добавляя или удаляя товар из избранного.
"use client";
import { useState } from "react";
const products = [
{ id: 1, name: "Кофе" },
{ id: 2, name: "Чай" },
{ id: 3, name: "Блокнот" },
];
export default function ProductsPage() {
const [favorites, setFavorites] = useState<number[]>([]);
function toggleFavorite(id: number) {
setFavorites((current) =>
current.includes(id)
? current.filter((item) => item !== id)
: [...current, id]
);
}
return (
<main>
<h1>Каталог</h1>
<p>В избранном: {favorites.length}</p>
<ul>
{products.map((product) => {
const isFavorite = favorites.includes(product.id);
return (
<li key={product.id}>
{product.name}{" "}
<button
type="button"
onClick={() => toggleFavorite(product.id)}
aria-pressed={isFavorite}
>
{isFavorite ? "Убрать" : "В избранное"}
</button>
</li>
);
})}
</ul>
</main>
);
}
В примере состояние favorites содержит массив идентификаторов товаров. Изначально массив пустой. После нажатия кнопки React обновляет его и повторно отображает компонент. Счётчик избранного, надпись кнопки и её состояние меняются без ручной работы с HTML-элементами. Строка "use client" нужна для такого интерактивного компонента в Next.js App Router. Хук useState работает на клиентской стороне.
Почему нельзя просто изменить массив
Посмотрите на функцию toggleFavorite. При добавлении товара мы создаём новый массив:
setFavorites((current) => [...current, id]);
При удалении используем filter, который тоже возвращает новый массив:
setFavorites((current) =>
current.filter((item) => item !== id)
);
А вот напрямую менять массив состояния через push не следует. Например, favorites.push(id) изменит существующий объект, но не сообщит React о новом значении состояния.
React рассматривает состояние как снимок данных на момент конкретного рендера. При обновлении массивов и объектов следует создавать новую версию значения, а не менять старую на месте. В результате поведение интерфейса становится понятнее. Есть прежнее состояние, действие пользователя и новое состояние, которое React отображает на экране.
Зачем передавать функцию в setFavorites
В примере мы используем не setFavorites(favorites), а функцию:
setFavorites((current) => {
return current.includes(id)
? current.filter((item) => item !== id)
: [...current, id];
});
current здесь означает актуальное предыдущее значение, на основе которого React рассчитывает следующее.
Такой способ особенно подходит, когда новое состояние зависит от предыдущего. Например, при увеличении счётчика или добавлении товара в список. Он помогает корректно обработать несколько обновлений, которые React может объединять. Если же новое значение не зависит от старого, можно передать его напрямую. Например, при выборе категории из списка вызвать setCategory("drinks").
Где заканчивается локальное состояние
useState хранит данные конкретного экземпляра компонента. Если открыть страницу заново, исходное состояние создаётся снова. В нашем примере массив избранного опять станет пустым. Это не ошибка хука. Локальное состояние предназначено для данных, с которыми компонент работает во время своего существования.
Если нужно сохранять избранное между посещениями, потребуется другой механизм. Для одного браузера это может быть localStorage. Для синхронизации между устройствами понадобится серверное хранилище и привязка данных к пользователю. При этом useState никуда не исчезает. Он по-прежнему может управлять текущим отображением кнопок и списков, а сервер отвечает за долговременное хранение.
В статье «Как использовать Supabase вместо отдельного backend» разобрано, как сохранять избранные записи в базе и ограничивать доступ к ним.
Когда useState не нужен
Не каждое значение на странице нужно хранить отдельно в состоянии. Например, количество избранных товаров уже можно получить из массива favorites.length. Создавать для него ещё один useState не требуется. Иначе придётся следить, чтобы счётчик и массив всегда изменялись согласованно. То же относится к отфильтрованному списку, который можно вычислить из исходных товаров и выбранной категории. Если результат полностью следует из имеющихся данных, отдельное состояние может только усложнить компонент.
А когда приложению нужны подписки на события браузера или синхронизация с внешней системой, появляется другая задача. Для неё используют useEffect, который разобран в статье «Почему useEffect в React запускается дважды».
От одной кнопки к приложению
В демонстрационном Goods Finder можно посмотреть каталог с поиском, маршрутизацией и пользовательскими действиями. Когда подобных элементов становится больше, разработчику приходится решать, какие данные хранить внутри компонента, какие передавать между компонентами, а какие получать с сервера.
В моей статье на Habr о localStorage в Next.js подробнее разобрано, как работать с состоянием браузера, не вызывая ошибок при серверном рендеринге.
Курс «Next.js I: JavaScript 2026» связывает основы React с App Router, загрузкой данных, URL-фильтрами и созданием приложения на Next.js.
useState нужен там, где компонент должен запомнить изменение и отразить его на экране. На примере избранного это видно без дополнительных библиотек. Пользователь нажимает кнопку, состояние меняется, React показывает новый результат.