Вы добавили на страницу каталог товаров, строку поиска и выбор категории. Пользователь настроил фильтры и увидел подходящие карточки. Затем обновил страницу, и всё сбросилось. Отправил ссылку другу, но у того открылся полный каталог. Так бывает, когда состояние поиска хранится только внутри React-компонента. Для поиска и фильтров есть другой вариант. Сохранять выбранные значения в адресе страницы.
Разберём, как это устроено в Next.js App Router.
Почему параметры поиска удобно хранить в URL
Посмотрите на адрес:
/catalog?q=coffee&category=drinks
Здесь /catalog обозначает страницу каталога, а часть после вопросительного знака содержит параметры поиска. q=coffee задаёт поисковый запрос, category=drinks ограничивает результаты категорией напитков. Такой адрес можно скопировать, сохранить в закладках или отправить другому человеку. При повторном открытии приложение получает те же параметры и может показать тот же набор результатов, если сами данные не изменились.
Параметры URL подходят для состояния, которым пользователь ожидает поделиться. Например, поискового запроса, категории, сортировки или номера страницы. А вот временное состояние кнопки или открытого меню не всегда нужно записывать в адрес.
Как Next.js получает параметры
В App Router страница может читать параметры через свойство searchParams. В современных версиях Next.js это асинхронное значение, поэтому внутри серверного компонента его получают через await.
Создадим каталог с несколькими товарами. Для начала не будем подключать базу данных. Нам достаточно массива, чтобы увидеть движение параметров.
// app/catalog/page.tsx
const products = [
{ id: 1, name: "Кофе", category: "drinks" },
{ id: 2, name: "Чай", category: "drinks" },
{ id: 3, name: "Блокнот", category: "office" },
];
export default async function CatalogPage({
searchParams,
}: {
searchParams: Promise<{
q?: string;
category?: string;
}>;
}) {
const { q = "", category = "" } = await searchParams;
const query = q.trim().toLowerCase();
const filtered = products.filter((product) => {
const matchesText = product.name
.toLowerCase()
.includes(query);
const matchesCategory =
!category || product.category === category;
return matchesText && matchesCategory;
});
return (
<main>
<h1>Каталог</h1>
<form action="/catalog" method="get">
<input
name="q"
defaultValue={q}
placeholder="Поиск товара"
/>
<select name="category" defaultValue={category}>
<option value="">Все категории</option>
<option value="drinks">Напитки</option>
<option value="office">Для офиса</option>
</select>
<button type="submit">Найти</button>
</form>
<ul>
{filtered.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>
</main>
);
}
После отправки формы браузер создаёт GET-запрос с параметрами q и category. Next.js читает их на странице, фильтрует массив и формирует результат.
Например, поиск слова кофе в категории «Напитки» оставит одну карточку. Если убрать категорию, поиск будет выполняться по всему каталогу. Здесь не потребовались useState, useEffect или дополнительная библиотека. Источником состояния служит URL, а форма передаёт значения обычным способом.
Почему фильтры не теряются после перезагрузки
Когда пользователь открывает /catalog?q=кофе&category=drinks, сервер получает значения из самого адреса. Даже после полной перезагрузки страницы они доступны снова. Форма также читает эти значения через defaultValue, поэтому введённый текст и выбранная категория отображаются при загрузке.
Кнопки браузера «Назад» и «Вперёд» работают с историей адресов. Пользователь может вернуться к предыдущему набору параметров, а страница снова покажет соответствующие результаты. Это отличие от состояния, которое живёт только в памяти React-компонента. При обычной перезагрузке такая память создаётся заново.
Как обновлять фильтры без отправки формы
Иногда нужно, чтобы результаты менялись сразу после выбора категории или ввода текста. Тогда можно использовать клиентские инструменты useSearchParams, usePathname и useRouter из next/navigation. useSearchParams читает параметры текущего URL. Для изменения адреса создают новый URLSearchParams на основе существующих значений, меняют нужный параметр и вызывают router.replace() или router.push().
Разница между ними связана с историей браузера. push() добавляет запись, к которой можно вернуться кнопкой «Назад». replace() заменяет текущую запись. Для каждого символа поисковой строки отдельная запись обычно не нужна. Если поиск обращается к серверу при вводе, запросы часто задерживают на небольшое время с помощью debounce. Это сокращает число обращений, пока пользователь набирает слово.
В нашем первом примере форма с GET решает задачу проще. Клиентские хуки понадобятся, когда появятся требования к обновлению без отправки формы.
Что делать с несколькими фильтрами
У каталога со временем появляются сортировка, минимальная цена и номер страницы. Их тоже можно передавать в адресе:
/catalog?q=coffee&category=drinks&sort=price&page=2
Но здесь появляется правило. Когда пользователь меняет поисковый запрос или категорию, номер страницы обычно нужно сбрасывать на первую. Иначе после смены фильтра можно попасть на страницу результатов, которой больше не существует.
Также стоит заранее определить допустимые значения параметров. Например, page=-5 не должен ломать интерфейс, а неизвестная категория не должна приводить к ошибке сервера. Если данные приходят из API, параметры проверяют и преобразуют до формирования запроса к источнику данных.
А как быть с SEO
Параметры URL сами по себе не означают, что поисковым системам нужно индексировать каждую комбинацию фильтров. Пользователю удобно делиться ссылкой на каталог с выбранными значениями. Но открывать отдельную индексируемую страницу для каждого сочетания цены, сортировки и категории обычно не требуется.
Это уже вопрос архитектуры индексации. В моей статье на Habr об URL как источнике состояния в Next.js подробно разобрана связь параметров адреса и интерфейса.
Здесь задача другая. Мы хотим, чтобы поиск работал предсказуемо для человека, а результаты можно было восстановить по ссылке.
От каталога к приложению
По мере развития каталога данные могут начать поступать с внешнего API, а фильтры станут зависеть друг от друга. При этом URL продолжит хранить параметры, необходимые для повторного открытия поиска.
В демонстрационном Goods Finder можно посмотреть приложение с каталогом, поиском, маршрутизацией и работой с параметрами адреса.
Курс «Next.js I: JavaScript 2026» разбирает App Router, маршруты, серверную загрузку данных, URL-фильтры и построение приложения на Next.js.
Для первого поиска достаточно формы с методом GET и чтения searchParams. Этого уже хватает, чтобы результаты не терялись при перезагрузке и открывались по сохранённой ссылке.