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

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

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

useUrlState — живое демо: next.js

Введите ниже — смотрите, как загорается URL

Первый клиентский компонент
Другой клиентский компонент

Читает из URL — без props, без context, типы и структура сохраняются

{
name: string
age: undefinedundefined
agree_to_terms: falseboolean
tags: []
}

Один API, три роутера

Быстрый старт

1. Определите состояние
state
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 } }[];
};
2. Оберните его в переиспользуемый hook
useFormState
'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 });
3. Используйте в любых компонентах — состояние общее
ComponentA
'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>
    </>
};
ComponentB
'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>
};
4. Расширяйте hook, когда нужно больше
useFormState - extended
'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 для layout
  • react-router-remix-setupНастройка React Router v6/v7 или Remix v2
  • form-library-integrationСовместное использование с react-hook-form (или formik)
  • shared-state-no-urluseSharedState — обмен без обращения к URL

Исходники лежат на GitHub. Агенту, который не умеет загружать навыки Intent, стоит прочитать llms.txt — те же правила, сжатые в один файл.

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 без настройки.

Читайте полное сравнение — одна фича в обеих библиотеках и как мигрировать

Почему 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 — звёздочка многое значит.

Поделитесь с другими разработчиками

Uneed Embed Badge