型付きの状態、住む場所は URL

useUrlState は、自分自身をクエリ文字列に書き込む React の状態です。オブジェクト、配列、日付は型を保ち、あらゆる状態は共有可能なリンクになり、リロード後も維持されます。プロバイダーもボイラープレートも不要。

  • ~2 KB gzip 圧縮
  • 依存関係ゼロ
  • TypeScript ファースト
  • Next.js / react-router / Remix
  • MIT
npm i state-in-url

useUrlState — ライブで試す: next.js

下に入力してください。URL が点灯するのを見てください

1 つ目のクライアントコンポーネント
もう一方のクライアントコンポーネント

URL から読み取ります。props も context も不要で、型と構造は保持されます

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

同じ API、3 つのルーター

クイックスタート

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 コーディングエージェントをお使いですか?

エージェントはここで毎回同じ 2 つのことを間違えます。状態の形を interface で定義しますが、 JSONCompatible 制約はそれを即座に拒否します。さらに、デフォルト状態のオブジェクトをコンポーネント内で構築すると、共有が静かに壊れます。オブジェクトの同一性でキー付けされるためエラーにはならず、2 つのコンポーネントが互いを見失うだけです。

このパッケージは 6 つの SKILL.md ファイルを同梱しています。エージェントは TanStack Intent を通じて必要に応じて 1 つを読み込みます。これらはこのページではなくライブラリと共にバージョン管理されます。

npx @tanstack/intent@latest install

すでに state-in-url がインストール済みのプロジェクトで一度実行します。エージェントは node_modules/state-in-url/skills/ の中のスキルを見つけます。

  • feature-state-hook状態の定義と、useUrlState を機能スコープのフックでラップする方法
  • input-handlingテキスト入力、スライダーなど、素早く変化するもの
  • nextjs-ssrApp Router: searchParams の転送、レイアウト用の Proxy
  • react-router-remix-setupReact Router v6/v7 または Remix v2 のセットアップ
  • form-library-integrationreact-hook-form(または formik)との組み合わせ
  • shared-state-no-urluseSharedState — URL に触れずに共有

ソースは GitHub にあります. Intent スキルを読み込めないエージェントは、代わりに llms.txt を読むべきです — 同じルールが 1 つのファイルに凝縮されています。

state-in-url vs nuqs

nuqs の代替をお探しですか?どちらも型付きの状態をクエリ文字列に保存しますが、必要な設定量と、値として扱えるものが異なります。

項目state-in-urlnuqs
セットアップ不要 — hook を import するだけアダプターコンポーネントでアプリをラップ
状態の形React.useState のような型付きオブジェクト1つキーごとの値、それぞれにパーサーを宣言
コンポーネント間の再利用hook を一度包むだけ — 全コンポーネントが状態を共有、props 不要パーサー群を包む hook は自分で抽出
ネストしたオブジェクトと配列標準対応 — 構造と型を保持JSON パーサーに加えて自前のバリデーターが必要
日付自動的に保持組み込みパーサーをキーごとに宣言
サイズ(全体 import)約 2.9 KB gzip約 6.7 KB gzip
ランタイム依存なし1つ
ルーターNext.js、React Router v6/v7、Remix、素の JS 用ヘルパーNext.js、React Router、Remix、TanStack Router、素の React

サイズはライブラリ全体の import を esbuild minify + gzip で計測(2026年8月、nuqs 2.10.1 と比較)。

nuqs も優れたライブラリです。値ごとに読みやすいクエリパラメータが欲しいとき、TanStack Router を使っているときは nuqs を。型付きオブジェクトを丸ごと URL に、設定ゼロで入れたいなら state-in-url を選んでください。

完全版の比較を読む — 同じ機能を両方で実装、移行方法も

なぜ state-in-url なのか?

URL 状態ライブラリは存在しますが、多くはセットアップが面倒だったり、保存できるものが限られていたりします。 state-in-url は「ただ動く」ことを目指しています。 React.useState を映す API で、URL をストアにします。

ボイラープレートなしで状態を保存し、ディープリンクを構築し、関連のないクライアントコンポーネント間でデータを共有できます。プロバイダーは不要です。構造と型は端から端まで保持されます。 Date が入り、 Date が出ます。

テストファーストで構築されており、ユニットテストとクロスブラウザの e2e スイートがすべてのコミットで実行されます。

Next.js: Suspense 境界は不要

このフックは useSearchParams を呼び出しません。そのため、このフックを使うコンポーネントは Suspense でラップする必要がなく、ページがプリレンダリングから除外されることもありません。PPR と cacheComponents も含みます。URL を直接読み取り、その後の変更をすべて追跡します。たとえば、 history.pushState を、その存在を知らないコードから呼ばれた場合も追跡します。

Next.js や react-router を使っていませんか?

この encodeState / decodeState ヘルパーは、どのフレームワークでもピュア JS でも動きます。フックはその上に載せた便利レイヤーです。

ぜひ GitHub ページ をご覧ください。スターをいただけると励みになります。

他の開発者と共有

Uneed Embed Badge