Типизированное состояние живёт в URL
useUrlState — это состояние React, которое само записывает себя в строку запроса. Объекты, массивы и даты сохраняют свои типы, любое состояние — это ссылка, которой можно поделиться, и оно переживает перезагрузку. Без провайдеров, без бойлерплейта.
- ~2 KB в gzip
- ноль зависимостей
- TypeScript-first
- Next.js / react-router / Remix
- MIT
npm i state-in-urlstate-in-url vs nuqs
Ищете альтернативу nuqs? Обе библиотеки хранят типизированное состояние в строке запроса; разница — в объёме настройки и в том, каким может быть значение.
| Что | state-in-url | nuqs |
|---|---|---|
| Настройка | Не нужна — импортируйте hook и работайте | Компонент-адаптер оборачивает приложение |
| Форма состояния | Один типизированный объект, как React.useState | Отдельные ключи, каждому объявляется парсер |
| Переиспользование в компонентах | Оберните hook один раз — каждый компонент разделяет состояние, без props | Собственный hook вокруг набора парсеров пишете сами |
| Вложенные объекты и массивы | Из коробки — структура и типы сохраняются | JSON-парсер плюс собственный валидатор |
| Даты | Сохраняются автоматически | Встроенный парсер, объявляется на каждый ключ |
| Размер, полный импорт | ~2,9 КБ gzip | ~6,7 КБ gzip |
| Зависимости в рантайме | Нет | Одна |
| Роутеры | Next.js, React Router v6/v7, Remix, хелперы для чистого JS | Next.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
import { NuqsAdapter } from 'nuqs/adapters/next/app';
export default function RootLayout({ children }) {
return (
<html>
<body>
<NuqsAdapter>{children}</NuqsAdapter>
</body>
</html>
);
}'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 })}
/>
);
};'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 Router | validateSearch на каждом роуте | 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 из коробки нет.
