Сайт использует сookies для хранения данных. Продолжая использовать сайт, вы даёте согласие на работу с этими файлами.

ОК
💻
Технологии
Опубликовано:
14.09.2026
Обновлено:
14.09.2026

URL-Driven State в React и Next.js: делаем фильтры типобезопасными с помощью nuqs

Тимофей Ищенко

Коротко: Разбираем 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).

Преимущества подхода:

  1. Шеринг ссылок (Shareable Links): пользователь может отправить точное состояние экрана коллеге или сохранить его в закладки.
  2. Предсказуемое обновление (Page Refresh): перезагрузка страницы не сбрасывает введенные данные.
  3. Нативная история браузера (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 повышает удобство использования продукта и устраняет необходимость синхронизировать параметры вручную.

Чек-лист для рефакторинга:

  1. Определите состояние для URL: выделите параметры, которые должны сохраняться при перезагрузке (поиск, фильтры, сортировка, пагинация, вкладки).
  2. Опишите схему через парсеры: откажитесь от ручного приведения типов в пользу parseAs*.
  3. Настройте историю переходов: используйте replace для инпутов с частым вводом и push для дискретных переключений.
  4. Удаляйте дефолтные значения: настраивайте .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) благодаря встроенным адаптерам маршрутизации.

Источники

Это авторская статья, основанная на личном опыте и субъективном взгляде автора. Заметили ошибку или битую ссылку? Сообщите нам: info@codesrc.ru - мы оперативно исправим. Спасибо, что помогаете делать блог лучше.
Следите за нами в соцсетях:

Читайте также