Типизированное состояние живёт в URL

useUrlState — это состояние React, которое само записывает себя в строку запроса. Объекты, массивы и даты сохраняют свои типы, любое состояние — это ссылка, которой можно поделиться, и оно переживает перезагрузку. Без провайдеров, без бойлерплейта.

  • ~2 KB в gzip
  • ноль зависимостей
  • TypeScript-first
  • Next.js / react-router / Remix
  • MIT
npm i state-in-url

state-in-url vs nuqs

Ищете альтернативу nuqs? Обе библиотеки хранят типизированное состояние в строке запроса; разница — в объёме настройки и в том, каким может быть значение.

Чтоstate-in-urlnuqs
НастройкаНе нужна — импортируйте hook и работайтеКомпонент-адаптер оборачивает приложение
Форма состоянияОдин типизированный объект, как React.useStateОтдельные ключи, каждому объявляется парсер
Переиспользование в компонентахОберните hook один раз — каждый компонент разделяет состояние, без propsСобственный hook вокруг набора парсеров пишете сами
Вложенные объекты и массивыИз коробки — структура и типы сохраняютсяJSON-парсер плюс собственный валидатор
ДатыСохраняются автоматическиВстроенный парсер, объявляется на каждый ключ
Размер, полный импорт~2,9 КБ gzip~6,7 КБ gzip
Зависимости в рантаймеНетОдна
РоутерыNext.js, React Router v6/v7, Remix, хелперы для чистого JSNext.js, React Router, Remix, TanStack Router, чистый React

Размеры: импорт всей библиотеки, esbuild minify + gzip, замер в августе 2026 против nuqs 2.10.1.

nuqs — достойная библиотека: берите её, если хотите отдельный читаемый query-параметр на каждое значение или используете TanStack Router. Берите state-in-url, когда нужен целый типизированный объект в URL без настройки.

Одна фича — в обеих

Панель фильтров: строка поиска, номер страницы, список тегов и дата. nuqs объявляет парсер на каждый ключ и подключает адаптер в корне; state-in-url берёт объект и оборачивает его в один переиспользуемый hook.

app/layout.tsx (nuqs)
// app/layout.tsx
import { NuqsAdapter } from 'nuqs/adapters/next/app';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <NuqsAdapter>{children}</NuqsAdapter>
      </body>
    </html>
  );
}
filters.tsx (nuqs)
'use client';
import {
  useQueryStates,
  parseAsString,
  parseAsInteger,
  parseAsArrayOf,
  parseAsIsoDateTime,
} from 'nuqs';

export const Filters = () => {
  const [filters, setFilters] = useQueryStates({
    q: parseAsString.withDefault(''),
    page: parseAsInteger.withDefault(1),
    tags: parseAsArrayOf(parseAsString).withDefault([]),
    since: parseAsIsoDateTime,
  });

  return (
    <input
      value={filters.q}
      onChange={(ev) => setFilters({ q: ev.target.value, page: 1 })}
    />
  );
};
filters.tsx (state-in-url)
'use client';
import { useUrlState } from 'state-in-url/next';

export const filters = {
  q: '',
  page: 1,
  tags: [] as string[],
  since: undefined as Date | undefined,
};

// One reusable hook = the whole API for this feature
export const useFilters = () => useUrlState(filters);

export const SearchBox = () => {
  const { urlState, setUrl } = useFilters();

  return (
    <input
      value={urlState.q}
      onChange={(ev) => setUrl({ q: ev.target.value, page: 1 })}
    />
  );
};

export const ActiveTags = () => {
  // Same state, another component - no props, no context
  const { urlState } = useFilters();

  return <>{urlState.tags.join(', ')}</>; // tags is still string[]
};

Этот один кастомный hook — весь API фичи: каждый компонент, который его вызывает, разделяет то же типизированное состояние — список тегов остаётся массивом, дата возвращается настоящим объектом Date. Без props, без context, без проводки по ключам.

Настройка и boilerplate

nuqs подключается к роутеру через компонент-адаптер, обёрнутый вокруг приложения, и каждый кусок состояния объявляет свой парсер. state-in-url даёт hook на каждый роутер — импортируйте подходящий, передайте объект состояния по умолчанию, готово. Ничто ничего не оборачивает.

Next.js, SSR и пререндеринг

В App Router state-in-url никогда не вызывает useSearchParams, поэтому компонентам не нужна граница Suspense — и страницы продолжают пререндериться, включая PPR. Серверные компоненты читают то же состояние через проп searchParams, переданный как есть.

Миграция с nuqs

Обычно миграция механическая: соберите ключи одной фичи в один объект состояния по умолчанию, уберите объявления парсеров — обычные типизированные значения несут ту же информацию — и замените посеттерные вызовы одним сеттером, принимающим partial. Каждое поле верхнего уровня по-прежнему отображается в свой query-параметр.

Как выглядят остальные варианты

nuqs — не единственная альтернатива. Ту же задачу — типизированное состояние в строке запроса — решают встроенные средства роутеров и библиотеки постарше, каждая со своим компромиссом.

БиблиотекаНастройкаВложенные объекты и датыРазмерКогда выбирать
state-in-urlНе нужна — импортируйте hookСохраняются автоматически, вместе с типами~2,9 КБ gzip, ноль зависимостейНужен один типизированный объект без настройки на Next.js, React Router или Remix
nuqsКомпонент-адаптер, парсер на каждый ключJSON-парсер плюс собственный валидатор~6,7 КБ gzip, одна зависимостьКаждое значение должно быть отдельным читаемым query-параметром
TanStack RoutervalidateSearch на каждом роутеJSON-first для объектов и массивов; даты — через свою сериализациюВстроено в роутерВы на TanStack Router — используйте встроенное
use-query-paramsПровайдер плюс адаптер роутера, конфиг на каждый параметрЧерез JSON-параметр, слабо типизировано~4,4 КБ gzip плюс serialize-query-paramsКодовая база уже построена на нём
useSearchParamsНе нужна — встроено в роутерТолько строки — парсинг, типы и значения по умолчанию на вас0 КБОдин-два плоских строковых параметра, библиотека не нужна

Частые вопросы

Хорошая ли state-in-url альтернатива nuqs?
Да, если нужен целый типизированный объект в URL без настройки: ни компонента-адаптера, ни парсеров на каждый ключ, а вложенные объекты и даты сохраняются автоматически. nuqs остаётся лучшим выбором, когда каждое значение должно быть отдельным читаемым query-параметром или вы на TanStack Router.
Что меньше — state-in-url или nuqs?
По замеру esbuild (minify + gzip, импорт всей библиотеки) в августе 2026: state-in-url — ~2,9 КБ и ноль зависимостей; nuqs 2.10.1 — ~6,7 КБ и одна зависимость. Частичный импорт уменьшает обе.
Нужен ли state-in-url адаптер или провайдер?
Нет. У каждого роутера свой entry point — импортируйте нужный hook, передайте объект состояния по умолчанию, и всё работает. Нет ни компонента-адаптера вокруг приложения, ни context-провайдера.
Сложно ли мигрировать с nuqs на state-in-url?
Обычно нет: соберите ключи фичи в один объект состояния по умолчанию, уберите объявления парсеров и замените посеттерные вызовы одним сеттером с partial. Каждое поле верхнего уровня по-прежнему отображается в свой query-параметр.
А как же search params в TanStack Router?
Если вы на TanStack Router — используйте встроенное: JSON-first search params с валидацией validateSearch на каждом роуте. state-in-url и nuqs нужны, когда ваш роутер — Next.js, React Router или Remix, где типизированных search params из коробки нет.