Типизированное состояние в URL для React и Next.js — как useState
useUrlState — это состояние React, которое само записывает себя в строку запроса. Объекты, массивы и даты сохраняют свои типы, любое состояние — это ссылка, которой можно поделиться, оно переживает перезагрузку, и кнопка «Назад» работает. Без провайдеров, без границы Suspense, без бойлерплейта.
- ~2 KB в gzip
- ноль зависимостей
- TypeScript-first
- Next.js / react-router / Remix / Astro
- MIT
npm i state-in-urluseUrlState — живое демо: next.js
Введите ниже — смотрите, как загорается URL
Читает из URL — без props, без context, типы и структура сохраняются
{name: stringage: undefinedundefinedagree_to_terms: falsebooleantags: []}
Управление состоянием в URL для Next.js, React Router, Remix и Astro — один API
Быстрый старт
export const form: Form = {
name: '',
age: undefined,
agree_to_terms: false,
tags: [],
};
// use `Type` not `Interface`!
type Form = {
name: string;
age?: number;
agree_to_terms: boolean;
tags: { id: string; value: { text: string; time: Date } }[];
};'use client';
import { useUrlState } from 'state-in-url/next';
import { form } from './form';
// One hook per feature - the whole API for this state
// "searchParams" only needed to pass params from Server Components
export const useFormState = (searchParams?: object) =>
useUrlState(form, { searchParams });'use client';
import { useFormState } from './useFormState';
export const ComponentA = () => {
// see docs for all possible params https://github.com/asmyshlyaev177/state-in-url/tree/master/packages/urlstate/next/useUrlState
const { urlState, setState, setUrl } = useFormState();
return <>
<input
id="name"
value={urlState.name}
onChange={(ev) => setUrl({ name: ev.target.value })}
/>
// OR can update state immediately but sync change to url as needed
<input
value={urlState.name}
onChange={(ev) => { setState(curr => ({ ...curr, name: ev.target.value })) }}
onBlur={() => setUrl()}
/>
<button onClick={() => setUrl((curr, initial) => initial)}>
Reset
</button>
</>
};'use client';
import { useFormState } from './useFormState';
// "searchParams" used to pass params from Server Components
export const ComponentB = ({ searchParams }: { searchParams?: object }) => {
// same state as ComponentA - no props, no context
const { urlState } = useFormState(searchParams);
// will be defaultValue from `form` if not in url, no need to check
return <div>name: {urlState.name}</div>
};'use client';
import React from 'react';
import { useUrlState } from 'state-in-url/next';
import { form } from './form';
export const useFormState = ({ searchParams }: { searchParams?: object }) => {
const { urlState, setUrl: setUrlBase, reset } = useUrlState(form, {
searchParams,
});
// first navigation will push new history entry
// all following will just replace that entry
// this way will have history with only 2 entries - ['/url', '/url?key=param']
const replace = React.useRef(false);
const setUrl = React.useCallback((
state: Parameters<typeof setUrlBase>[0],
opts?: Parameters<typeof setUrlBase>[1]
) => {
setUrlBase(state, { replace: replace.current, ...opts });
replace.current = true;
}, [setUrlBase]);
return { urlState, setUrl, resetUrl: reset };
};Пользуетесь AI-агентом для кодинга?
Агенты каждый раз совершают здесь одни и те же две ошибки. Они описывают форму состояния через interface, а ограничение JSONCompatible его сразу отвергает. И они создают объект состояния по умолчанию внутри компонента, что незаметно ломает обмен — он адресуется по идентичности объекта, поэтому ошибки нет, просто два компонента перестают видеть друг друга.
Поэтому пакет поставляется с семью SKILL.md -файлами. Ваш агент загружает один по запросу через TanStack Intent, и они версионируются вместе с библиотекой, а не с этой страницей.
npx @tanstack/intent@latest installЗапустите один раз в проекте, где уже установлен state-in-url . После этого агент найдёт навыки в node_modules/state-in-url/skills/.
feature-state-hookОпределение состояния и оборачивание useUrlState в hook с областью фичиinput-handlingТекстовые поля, слайдеры — всё, что меняется быстроnextjs-ssrApp Router: проброс searchParams, Proxy для layoutreact-router-remix-setupНастройка React Router v6/v7 или Remix v2astro-setupОстрова Astro (React или Preact) либо страницы без клиентского фреймворкаform-library-integrationСовместное использование с react-hook-form (или formik)shared-state-no-urluseSharedState — обмен без обращения к URL
Исходники лежат на GitHub. Агенту, который не умеет загружать навыки Intent, стоит прочитать llms.txt — те же правила, сжатые в один файл.
state-in-url vs nuqs
Ищете альтернативу nuqs? Обе библиотеки хранят типизированное состояние в строке запроса; разница — в объёме настройки и в том, каким может быть значение.
| Что | state-in-url | nuqs |
|---|---|---|
| Настройка | Next.js, React Router v6/v7, Remix, Astro, хелперы для чистого JS | Компонент-адаптер оборачивает приложение |
| Форма состояния | Один типизированный объект, как 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 без настройки.
Читайте полное сравнение — одна фича в обеих библиотеках и как мигрировать
Состояние в URL в React — частые вопросы
- Зачем хранить состояние React в URL?
- URL, в котором лежит состояние, — это ссылка, которой можно поделиться: перезагрузите её, добавьте в закладки или отправьте — откроются те же фильтры, вкладка или страница. Кнопки «Назад» и «Вперёд» работают бесплатно, а несвязанные компоненты читают одни и те же значения без провайдера. state-in-url делает это одним типизированным объектом вместо строк, разобранных вручную.
- Какое состояние стоит хранить в URL?
- Всё, что читатель может добавить в закладки или отправить: фильтры, сортировку, пагинацию, активную вкладку, диапазон дат, текст поиска. Не кладите туда приватное, огромное или сугубо временное — auth-токены, открыт ли диалог, положение мыши. Быстрая проверка: будет ли ссылка с этим значением иметь смысл для того, кому вы её отправили?
- Как читать и менять параметры URL в React с state-in-url?
- Вызовите useUrlState с объектом состояния по умолчанию. urlState хранит текущие значения, уже типизированные; setUrl записывает partial-объект в строку запроса; setState обновляет состояние, не трогая URL, пока вы явно не запишете его туда. Числа, булевы значения, массивы, вложенные объекты и Date возвращаются теми же типами, какими вошли.
- Переживает ли состояние в URL перезагрузку страницы?
- Да. Состояние — это и есть строка запроса, поэтому перезагрузка, закладка или ссылка, вставленная в другом месте, восстанавливают его. В Next.js App Router передайте в hook проп searchParams страницы, чтобы уже первый серверный рендер показывал правильные значения, а не значения по умолчанию.
- Работает ли это с Next.js Server Components без границы Suspense?
- Да. Этот hook никогда не вызывает useSearchParams, поэтому использующему его компоненту не нужна граница Suspense и он не исключает страницу из пререндеринга, включая PPR. Server Components читают то же состояние через проп searchParams; layout может декодировать его из заголовка, выставленного в proxy.ts.
- Можно ли синхронизировать react-hook-form или табличную библиотеку с URL?
- Да. Оставьте библиотеку форм источником истины, инициализируйте её значениями из urlState как значениями по умолчанию и отражайте её изменения через setUrl из обработчика изменений или эффекта. Тот же паттерн работает для состояния TanStack Table, панелей фильтров и всего остального, что отдаёт значения и сеттер.
- Какие фреймворки поддерживает state-in-url?
- Next.js 14–16 App Router, React Router v6 и v7, Remix v2 и острова Astro (React или Preact) — у каждого свой entry point. Чистый JavaScript и любой другой фреймворк могут напрямую использовать хелперы encodeState и decodeState. Библиотека весит ~2 KB в gzip и не имеет зависимостей.
Почему state-in-url?
Библиотеки состояния в URL существуют, но большинство либо громоздки в настройке, либо ограничены в том, что могут хранить. state-in-url стремится быть той, что просто работает: API, повторяющий React.useState, с URL в качестве хранилища.
Храните состояние без бойлерплейта, стройте глубокие ссылки и делитесь данными между несвязанными клиентскими компонентами — провайдер не нужен. Структура и типы сохраняются от начала до конца: Date входит, а Date выходит.
Собрано по принципу test-first, юнит-тесты и кросс-браузерные e2e-наборы запускаются на каждом коммите.
Next.js: граница Suspense не нужна
Этот hook никогда не вызывает useSearchParams, поэтому использующему его компоненту не нужна обёртка в Suspense и он не исключает страницу из предварительного рендеринга — включая PPR и cacheComponents . Он читает URL напрямую и отслеживает каждое последующее изменение, включая history.pushState из кода, который о нём ничего не знает.
Не на Next.js или react-router?
Вспомогательные функции encodeState / decodeState работают с любым фреймворком или чистым JS — hook'и это лишь удобная обёртка поверх.
Загляните на страницу на GitHub — звёздочка многое значит.
Поделитесь с другими разработчиками

