События и состояние React
Добавляем счетчик и фильтр, разбираем очередь обновлений.
На этой странице
Для Next.js 16.3.8 (App Router) и React 19.3.0. Обновлено .
В обычном JavaScript изменение переменной само по себе не обновляло DOM: мы вызывали render. В React состояние хранится через useState. Функция обновления состояния сообщает React, что интерфейс нужно построить заново с новым значением.
useState – хук, функция React для подключения возможностей компонента. Хуки вызываются на верхнем уровне компонента, до условных return, а не внутри if, циклов или обработчиков.
Счетчик и его подключение
Создайте клиентский компонент:
"use client";
import { useState } from "react";
export function TicketCounter() {
const [count, setCount] = useState(1);
return (
<div>
<p>Билетов: {count}</p>
<button type="button" onClick={() => setCount((current) => current + 1)}>
Добавить билет
</button>
</div>
);
}useState(1) задает начальное значение. Деструктуризация массива дает два элемента: count – текущее значение, setCount – функция обновления. Названия выбирает разработчик.
onClick получает функцию, которую React вызовет после нажатия. Запись onClick={setCount(2)} вызвала бы обновление сразу при отображении; передавайте функцию, например onClick={() => setCount(2)}.
Подключите счетчик:
import { TicketCounter } from "./ticket-counter";
export default function HomePage() {
return (
<main>
<h1>Куда пойти</h1>
<TicketCounter />
</main>
);
}"use client" нужна компоненту с состоянием и событиями. Страница может оставаться серверной: подробнее разберем эту границу во втором этапе.
Почему обычная переменная не подходит
Локальная переменная let count = 1 создается заново при каждом вызове компонента. Изменение этой переменной в обработчике не запускает обновление интерфейса. А useState сохраняет значение между вызовами компонента и дает функцию для его обновления.
Обновления попадают в очередь
Три вызова в одном нажатии
Одинаковое значение три раза
setCount(count + 1);setCount(count + 1);setCount(count + 1);Первый счетчик: 0
Каждый раз берем предыдущий результат
setCount((current) => current + 1);setCount((current) => current + 1);setCount((current) => current + 1);Второй счетчик: 0
Слева трижды записывается одно число. Справа каждое обновление прибавляет единицу к результату предыдущего. React выполняет эти обработчики в самой демонстрации.
В этом сравнении оба обработчика трижды вызывают обновление состояния. Нажмите кнопки и сравните результат: вариант с функцией использует результат предыдущего обновления.
setCount(count + 1) тоже допустима для одного обычного обновления. Но несколько таких вызовов в одном обработчике используют одно и то же значение count. Новое значение станет доступно при следующем вызове компонента. В уже работающем обработчике переменная останется прежней.
Фильтр без лишнего состояния
Самостоятельный пример заменяет app/page.tsx:
"use client";
import { useState } from "react";
const events = [
{ id: 1, name: "Кинопоказ", price: 300 },
{ id: 2, name: "Лекция", price: 0 },
];
export default function HomePage() {
const [freeOnly, setFreeOnly] = useState(false);
const visible = freeOnly
? events.filter((event) => event.price === 0)
: events;
return (
<main>
<button
type="button"
aria-pressed={freeOnly}
onClick={() => setFreeOnly((current) => !current)}
>
Только бесплатные
</button>
{visible.length === 0 ? (
<p>Ничего не найдено.</p>
) : (
<ul>
{visible.map((event) => (
<li key={event.id}>{event.name}</li>
))}
</ul>
)}
</main>
);
}Храним только выбор пользователя. visible вычисляется из него при отображении. Если хранить отфильтрованный список отдельно, придется обновлять и его, и фильтр. Один из них легко забыть обновить. useEffect для такого вычисления не нужен.
Объекты и массивы в состоянии
Создавайте новое значение вместо изменения существующего: setEvent((current) => ({ ...current, name: "Новое название" })). Для списка можно использовать map: вернуть новый объект для нужного элемента, а остальные оставить прежними. Простое event.name = ... не сообщает React об обновлении.
Если несколько компонентов используют один фильтр, храните его у ближайшего общего родителя и передавайте значение и обработчик через props. При удалении компонента его локальное состояние теряется; перезагрузка страницы тоже не сохраняет выбор автоматически.
Попробуйте: добавьте кнопку уменьшения количества. Не разрешайте число меньше одного. Затем вынесите фильтр в отдельный компонент: родитель хранит выбор и передает его дочернему.
Документация: события, useState, состояние как снимок, обновления в очереди.