Коротко: Разбираем URL-Driven State в React и Next.js. Как библиотека nuqs обеспечивает типобезопасность фильтров, пагинации и поиска без бойлерплейта.
Каждый фронтенд-разработчик сталкивался с ситуацией: пользователь настраивает фильтры в каталоге, переходит на десятую страницу, копирует ссылку, отправляет коллеге — а тот открывает дефолтный список без единого примененного параметра. Причина проста: состояние фильтрации изолировано внутри локального useState или клиентского хранилища (Redux, Zustand) и никак не связано с адресной строкой.
Перенос состояния в URL решает эту проблему, превращая ссылку в единственный источник правды (Single Source of Truth). Однако реализация такого подхода стандартными средствами React и Next.js часто обрастает десятками строк бойлерплейта, ручным парсингом строк и ошибками типизации.
Библиотека nuqs (ранее известная как Next URL Query State) решает эти задачи, предоставляя привычный интерфейс useState, строгую валидацию и автоматическую синхронизацию с адресной строкой браузера.
Что такое URL-Driven State и зачем он нужен
Концепция URL-Driven State заключается в том, что любое состояние интерфейса, влияющее на отображение данных (фильтры, сортировки, поиск, номер страницы, активная вкладка), хранится непосредственно в параметрах запроса (query params).
Преимущества подхода:
- Шеринг ссылок (Shareable Links): пользователь может отправить точное состояние экрана коллеге или сохранить его в закладки.
- Предсказуемое обновление (Page Refresh): перезагрузка страницы не сбрасывает введенные данные.
- Нативная история браузера (Back/Forward): кнопки «Назад» и «Вперед» работают предсказуемо, возвращая пользователя к предыдущему набору фильтров.
Ограничения нативного useSearchParams
В Next.js хук useSearchParams возвращает объект URLSearchParams, работающий только в режиме чтения. Чтобы обновить хотя бы один параметр, разработчику приходится:
- Вручную копировать текущие параметры в новый экземпляр
URLSearchParams. - Преобразовывать числа, массивы или булевы флаги в строки.
- Вызывать методы роутера (
router.pushилиrouter.replace). - Обрабатывать краевые случаи: удаление пустых параметров, предотвращение лишних перерисовок и рассинхронизацию типов в TypeScript.
Такой подход быстро превращается в трудноподдерживаемый техдолг.
Знакомство с nuqs: философия и базовая модель
Библиотека nuqs создана для того, чтобы работа с searchParams выглядела как работа со стандартным состоянием React, но с автоматической записью изменений в адресную строку.
Базовый хук useQueryState
Хук useQueryState принимает имя параметра и возвращает кортеж из текущего значения и функции-сеттера:
'use client';
import { useQueryState } from 'nuqs';
export function SearchInput() {
const [query, setQuery] = useQueryState('q');
return (
<input
type="text"
value={query ?? ''}
onChange={(e) => setQuery(e.target.value || null)}
placeholder="Поиск по названию..."
/>
);
}
Поведение с null и очистка URL
Если параметр отсутствует в URL, хук возвращает null. Передача null в функцию обновления удаляет query-параметр из адресной строки, избавляя URL от мусорных конструкций вроде ?q=&category=.
Типобезопасность и парсинг данных
Строка запроса в браузере всегда оперирует исключительно строковыми типами (string). При попытке передать числа, списки идентификаторов или даты разработчик сталкивается с проблемой рассинхронизации типов («URL Type-Safety Iceberg»): TypeScript считает тип числом, а из адресной строки приходит строка, требующая валидации в рантайме.
nuqs решает эту задачу с помощью встроенных парсеров, обеспечивающих взаимно-однозначное соответствие (биективность) между значением в JavaScript и строкой в URL.
'use client';
import {
useQueryState,
parseAsInteger,
parseAsBoolean,
parseAsStringEnum,
parseAsArrayOf
} from 'nuqs';
// Парсер для пагинации с дефолтным значением
const [page, setPage] = useQueryState('page', parseAsInteger.withDefault(1));
// Парсер для булева флага
const [inStock, setInStock] = useQueryState('inStock', parseAsBoolean.withDefault(false));
// Парсер для строкового Enum
enum SortOrder {
ASC = 'asc',
DESC = 'desc'
}
const [sort, setSort] = useQueryState('sort', parseAsStringEnum(Object.values(SortOrder)));
// Парсер для массива чисел (например, ID выбранных тегов: ?tags=1,2,3)
const [tags, setTags] = useQueryState('tags', parseAsArrayOf(parseAsInteger));
Если пользователь вручную введет в URL некорректное значение (например, ?page=invalid_number), парсер отбросит его и безопасно вернет значение по умолчанию или null, предотвратив падение приложения.
Практический пример: сложная форма фильтрации через useQueryStates
Когда на странице присутствует множество взаимосвязанных параметров (каталог товаров, аналитическая таблица), вызывать useQueryState для каждого поля неэффективно. Для этого используется хук useQueryStates, объединяющий схему фильтров в единый объект.
Определение схемы и компонента
'use client';
import { useQueryStates, parseAsString, parseAsInteger, parseAsStringEnum } from 'nuqs';
const filterParsers = {
search: parseAsString.withDefault(''),
category: parseAsString.withDefault('all'),
minPrice: parseAsInteger.withDefault(0),
maxPrice: parseAsInteger.withDefault(100000),
sortBy: parseAsStringEnum(['price_asc', 'price_desc', 'date']).withDefault('date'),
page: parseAsInteger.withDefault(1),
};
export function CatalogFilters() {
const [filters, setFilters] = useQueryStates(filterParsers, {
history: 'push', // Добавляет запись в историю браузера
shallow: true, // Клиентское обновление без лишних серверных запросов
});
const handleCategoryChange = (category: string) => {
// При смене категории сбрасываем пагинацию на первую страницу
setFilters({
category,
page: 1,
});
};
return (
<div className="filters-panel">
<input
type="text"
value={filters.search}
onChange={(e) => setFilters({ search: e.target.value || null, page: 1 }, { history: 'replace' })}
placeholder="Поиск..."
/>
<select
value={filters.category}
onChange={(e) => handleCategoryChange(e.target.value)}
>
<option value="all">Все категории</option>
<option value="electronics">Электроника</option>
<option value="books">Книги</option>
</select>
<button onClick={() => setFilters(null)}>
Сбросить все фильтры
</button>
</div>
);
}
Управление историей: push против replace
При вводе текста в поисковую строку добавление каждого символа в стек истории браузера создает спам, из-за которого кнопка «Назад» перестает нормально работать. В nuqs это решается передачей опции { history: 'replace' } для текстовых полей и { history: 'push' } для явных действий (выбор категории, переключение страниц).
nuqs в архитектуре Next.js: SSR и Server Components
nuqs корректно интегрируется как в Next.js App Router, так и в Pages Router, предотвращая ошибки гидратации (hydration mismatch).
Для работы хуков на клиенте необходимо обернуть приложение или сегмент дерева компонентов в NuqsAdapter:
// app/layout.tsx
import { NuqsAdapter } from 'nuqs/adapters/next/app';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="ru">
<body>
<NuqsAdapter>{children}</NuqsAdapter>
</body>
</html>
);
}
Для чтения параметров в серверных компонентах (Server Components) nuqs предоставляет утилиты валидации и создания загрузчиков (например, createLoader), что позволяет использовать один и тот же контракт парсеров как на сервере, так и на клиенте.
Итоги и чек-лист для внедрения
Перевод фильтров и параметров интерфейса на URL-Driven State повышает удобство использования продукта и устраняет необходимость синхронизировать параметры вручную.
Чек-лист для рефакторинга:
- Определите состояние для URL: выделите параметры, которые должны сохраняться при перезагрузке (поиск, фильтры, сортировка, пагинация, вкладки).
- Опишите схему через парсеры: откажитесь от ручного приведения типов в пользу
parseAs*. - Настройте историю переходов: используйте
replaceдля инпутов с частым вводом иpushдля дискретных переключений. - Удаляйте дефолтные значения: настраивайте
.withDefault(), чтобы не загромождать URL лишними параметрами при стандартном состоянии экрана.
Часто задаваемые вопросы (FAQ)
Чем nuqs лучше нативного хука useSearchParams из Next.js?
Нативный useSearchParams доступен только для чтения и возвращает строковые значения. Чтобы обновить параметр, нужно вручную собирать строку запроса, преобразовывать типы и вызывать роутер. nuqs объединяет чтение, типизацию, валидацию и запись в один вызов, аналогичный useState.
Создает ли каждое изменение фильтра новую запись в истории браузера?
Поведение настраивается гибко. Для частых изменений (например, ввод текста в поисковой строке) используется режим history: 'replace', который перезаписывает текущий URL без засорения истории переходов. Для переключения страниц или категорий подходит history: 'push'.
Как nuqs работает при серверном рендеринге (SSR / RSC)?
Библиотека синхронизирует состояние между сервером и клиентом. Обертка NuqsAdapter обеспечивает корректную гидратацию клиентских компонентов, предотвращая несоответствие разметки между сервером и браузером.
Что происходит, если пользователь вручную ввел в URL некорректное значение?
Встроенные парсеры проверяют входящие данные в рантайме. Если значение не соответствует типу (например, текст в параметре parseAsInteger), nuqs игнорирует некорректную строку и возвращает значение по умолчанию или null.
Подходит ли nuqs только для Next.js или работает с чистым React?
Несмотря на название (Next URL Query State), библиотека поддерживает не только Next.js (App и Pages Router), но и чистый React (в связке с Vite, React Router, TanStack Router или Remix) благодаря встроенным адаптерам маршрутизации.




.svg.webp)





