state-in-url 6.4.1 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.es.md +214 -7
  2. package/README.fr.md +214 -7
  3. package/README.ja.md +212 -5
  4. package/README.ko.md +212 -5
  5. package/README.md +212 -6
  6. package/README.pt-BR.md +214 -7
  7. package/README.ru.md +214 -7
  8. package/README.vi.md +214 -7
  9. package/README.zh-CN.md +214 -7
  10. package/dist/astro/index.d.ts +1 -0
  11. package/dist/astro/index.js +1 -0
  12. package/dist/astro/index.mjs +1 -0
  13. package/dist/astro/useUrlState/index.d.ts +1 -0
  14. package/dist/astro/useUrlState/index.js +1 -0
  15. package/dist/astro/useUrlState/index.mjs +1 -0
  16. package/dist/astro/useUrlState/useUrlState.d.ts +1 -0
  17. package/dist/encodeState/encodeState.d.ts +1 -1
  18. package/dist/encodeState/encodeState.js +1 -1
  19. package/dist/encodeState/encodeState.mjs +1 -1
  20. package/dist/index.d.ts +1 -2
  21. package/dist/index.js +1 -1
  22. package/dist/index.mjs +1 -1
  23. package/dist/next/useUrlState/useUrlState.d.ts +3 -3
  24. package/dist/next/useUrlState/useUrlState.js +1 -1
  25. package/dist/next/useUrlState/useUrlState.mjs +1 -1
  26. package/dist/package.json +68 -16
  27. package/dist/react/index.d.ts +1 -0
  28. package/dist/react/index.js +1 -0
  29. package/dist/react/index.mjs +1 -0
  30. package/dist/react/useUrlState/index.d.ts +1 -0
  31. package/dist/react/useUrlState/index.js +1 -0
  32. package/dist/react/useUrlState/index.mjs +1 -0
  33. package/dist/react/useUrlState/useUrlState.d.ts +86 -0
  34. package/dist/react/useUrlState/useUrlState.js +1 -0
  35. package/dist/react/useUrlState/useUrlState.mjs +1 -0
  36. package/dist/react-router/useUrlState/useUrlState.d.ts +5 -5
  37. package/dist/react-router/useUrlState/useUrlState.js +1 -1
  38. package/dist/react-router/useUrlState/useUrlState.mjs +1 -1
  39. package/dist/react-router6/useUrlState/useUrlState.d.ts +7 -7
  40. package/dist/react-router6/useUrlState/useUrlState.js +1 -1
  41. package/dist/remix/useUrlState/useUrlState.d.ts +5 -5
  42. package/dist/useIsomorphicLayoutEffect.js +1 -1
  43. package/dist/useIsomorphicLayoutEffect.mjs +1 -1
  44. package/dist/useLinkProps/useLinkProps.mjs +1 -1
  45. package/dist/useUrlEncode/useUrlEncode.js +1 -1
  46. package/dist/useUrlStateBase/useUrlStateBase.js +1 -1
  47. package/package.json +68 -16
  48. package/skills/astro-setup/SKILL.md +204 -0
  49. package/skills/feature-state-hook/SKILL.md +1 -1
  50. package/skills/form-library-integration/SKILL.md +1 -1
  51. package/skills/input-handling/SKILL.md +1 -1
  52. package/skills/nextjs-ssr/SKILL.md +1 -1
  53. package/skills/react-no-router/SKILL.md +215 -0
  54. package/skills/react-router-remix-setup/SKILL.md +1 -1
  55. package/skills/shared-state-no-url/SKILL.md +1 -1
package/README.ja.md CHANGED
@@ -1,6 +1,6 @@
1
1
  <!-- i18n:start -->
2
2
  [English](./README.md) · [简体中文](./README.zh-CN.md) · 日本語 · [한국어](./README.ko.md) · [Русский](./README.ru.md) · [Español](./README.es.md) · [Português (BR)](./README.pt-BR.md) · [Français](./README.fr.md) · [Tiếng Việt](./README.vi.md)
3
- <!-- i18n:meta locale=ja source=README.md source-blob=bbf45a10aae9e88f68b27d84f51ee3e7e8a1e436 status=translated -->
3
+ <!-- i18n:meta locale=ja source=README.md source-blob=ad78fdbbee096115a953813ed9a462e3393c5736 status=translated -->
4
4
  <!-- i18n:end -->
5
5
 
6
6
  <div align="center">
@@ -69,7 +69,7 @@
69
69
  任意のユーザー状態をクエリパラメータに保存します。ブラウザの URL に JSON があるイメージです。データの型と構造を保持したまま、たとえば数値は文字列ではなく数値として、日付は日付としてデコードされ、オブジェクトや配列もサポートされます。
70
70
  非常にシンプルで高速、そして静的 TypeScript バリデーション付き。ディープリンク(別名 URL 同期)が簡単になります。
71
71
 
72
- Next.js と react-router 用の `useUrlState` フックと、その他あらゆる JS 向けのヘルパーを備えています。
72
+ Next.js、react-router、Remix、Astro 用の `useUrlState` フックと、その他あらゆる JS 向けのヘルパーを備えています。
73
73
  現代のブラウザは巨大な URL をサポートしており、ユーザーはクエリ文字列を気にしません(全選択してコピー/ペーストするワークフローです)。
74
74
 
75
75
  本来の目的どおりに、クエリ文字列を状態管理に使うときが来ました。
@@ -98,10 +98,32 @@ Next.js と react-router 用の `useUrlState` フックと、その他あらゆ
98
98
  - **サーバーサイドレンダリング**: サーバーコンポーネントで使用可能。Next.js 14、15、16 をサポート
99
99
  - **軽量**: 依存関係ゼロ。ライブラリは 2KB 未満
100
100
  - **DX**: 優れた開発者体験。ドキュメント、JSDoc コメント、サンプル付き
101
- - **フレームワークの柔軟性**: `Next.js` と `react-router` 用のフック、その他のフレームワークやピュア JS で使うためのヘルパー
101
+ - **フレームワークの柔軟性**: `Next.js`、`react-router`、`Remix`、`Astro` 用のフック、その他のフレームワークやピュア JS で使うためのヘルパー
102
102
  - **十分にテスト済み**: [Chrome/Firefox/Safari 向けのユニットテストと Playwright テスト](https://github.com/asmyshlyaev177/state-in-url/actions/workflows/tests.yml)
103
103
  - **寛容なライセンス**: MIT
104
104
 
105
+ ### nuqs との比較
106
+
107
+ [nuqs](https://github.com/47ng/nuqs) の代替をお探しですか?どちらも型付きの状態をクエリ文字列に保存しますが、必要な設定量と、値として扱えるものが異なります。
108
+
109
+ | 項目 | `state-in-url` | `nuqs` |
110
+ | --- | --- | --- |
111
+ | セットアップ | 不要 — hook を import するだけ | アダプターコンポーネントでアプリをラップ |
112
+ | 状態の形 | `React.useState` のような型付きオブジェクト1つ | キーごとの値、それぞれにパーサーを宣言 |
113
+ | コンポーネント間の再利用 | hook を一度包むだけ — 全コンポーネントが状態を共有、props 不要 | パーサー群を包む hook は自分で抽出 |
114
+ | ネストしたオブジェクトと配列 | 標準対応 — 構造と型を保持 | JSON パーサーに加えて自前のバリデーターが必要 |
115
+ | 日付 | 自動的に保持 | 組み込みパーサーをキーごとに宣言 |
116
+ | サイズ(全体 import) | 約 2.9 KB gzip | 約 6.7 KB gzip |
117
+ | ランタイム依存 | なし | 1つ |
118
+ | ルーター | Next.js、React Router v6/v7、Remix、Astro、素の JS 用ヘルパー | Next.js、React Router、Remix、TanStack Router、素の React |
119
+
120
+ サイズはライブラリ全体の import を esbuild minify + gzip で計測(2026年8月、nuqs 2.10.1 と比較)。
121
+
122
+ nuqs も優れたライブラリです。値ごとに読みやすいクエリパラメータが欲しいとき、TanStack Router を使っているときは nuqs を。型付きオブジェクトを丸ごと URL に、設定ゼロで入れたいなら state-in-url を選んでください。
123
+
124
+
125
+ 完全版の比較 — 同じ機能の両実装、ほかの代替(TanStack Router、use-query-params)、移行メモ — は <https://state-in-url.dev/vs/nuqs> にあります。
126
+
105
127
  ## 目次
106
128
 
107
129
  <!-- toc:start -->
@@ -111,6 +133,7 @@ Next.js と react-router 用の `useUrlState` フックと、その他あらゆ
111
133
  - [なぜ `state-in-url` を使うのか?](#なぜ-state-in-url-を使うのか)
112
134
  - [ユースケース](#ユースケース)
113
135
  - [特徴](#特徴)
136
+ - [nuqs との比較](#nuqs-との比較)
114
137
  - [目次](#目次)
115
138
  - [インストール](#インストール)
116
139
  - [1. パッケージのインストール](#1-パッケージのインストール)
@@ -127,6 +150,10 @@ Next.js と react-router 用の `useUrlState` フックと、その他あらゆ
127
150
  - [例](#例)
128
151
  - [React-Router 向け useUrlState フック](#react-router-向け-useurlstate-フック)
129
152
  - [例](#例-1)
153
+ - [React 向け useUrlState フック(ルーターなし)](#react-向け-useurlstate-フックルーターなし)
154
+ - [例](#例-2)
155
+ - [Astro 向け useUrlState フック](#astro-向け-useurlstate-フック)
156
+ - [例](#例-3)
130
157
  - [レシピ](#レシピ)
131
158
  - [状態の一部を便利に扱うカスタムフック](#状態の一部を便利に扱うカスタムフック)
132
159
  - [複雑な状態の形を使用](#複雑な状態の形を使用)
@@ -515,6 +542,156 @@ const tags = [
515
542
 
516
543
  [サンプルコード](packages/example-react-router6/src/Form-for-test.tsx)
517
544
 
545
+ ### React 向け useUrlState フック(ルーターなし)
546
+
547
+ ルーターを持たない React アプリ向けです。Vite、Create React App、あるいは自分では管理できないページに埋め込むウィジェットなどが対象です。このフックは `window.history` で URL を書き込み、戻る/進むや他の `pushState`/`replaceState` のたびに読み戻します。同じ state オブジェクトを使うコンポーネントはすべて状態を共有するため、ラップするものも props で渡すものもありません。
548
+
549
+ [API ドキュメント](packages/urlstate/react/useUrlState)
550
+
551
+ #### 例
552
+
553
+ ```typescript
554
+ // useFilters.ts
555
+ import { useUrlState } from 'state-in-url/react';
556
+
557
+ type FiltersState = { sort: 'name' | 'date'; page: number };
558
+ const FILTERS_STATE: FiltersState = { sort: 'name', page: 1 };
559
+
560
+ export function useFilters() {
561
+ return useUrlState(FILTERS_STATE);
562
+ }
563
+ ```
564
+
565
+ ```typescript
566
+ // FiltersBar.tsx
567
+ import { useFilters } from './useFilters';
568
+
569
+ export function FiltersBar() {
570
+ const { urlState, setUrl } = useFilters();
571
+
572
+ return <button onClick={() => setUrl({ page: urlState.page + 1 })}>Page {urlState.page}</button>;
573
+ }
574
+ ```
575
+
576
+ `state-in-url/astro` は同じフックです。islands では `searchParams` を prop として受け取るため、下で別に説明しています。
577
+
578
+ ### Astro 向け useUrlState フック
579
+
580
+ React アイランド向けです。Astro にはデフォルトでクライアントサイドルーターがないため、このフックは `window.history` で URL を書き込み、戻る/進む操作時およびその他あらゆる `pushState`/`replaceState` の際(Astro 自身の `<ClientRouter />` を含む)に URL を読み戻します。ページ上のアイランドは状態を共有し、それらをラップするものは何も必要ありません。
581
+
582
+ [API ドキュメント](packages/urlstate/astro/useUrlState)
583
+
584
+ #### 例
585
+
586
+ ```typescript
587
+ // src/state.ts
588
+ export const form: Form = {
589
+ name: '',
590
+ age: undefined,
591
+ agree_to_terms: false,
592
+ tags: [],
593
+ };
594
+
595
+ type Form = {
596
+ name: string;
597
+ age?: number;
598
+ agree_to_terms: boolean;
599
+ tags: { id: string; value: { text: string; time: Date } }[];
600
+ };
601
+ ```
602
+
603
+ ページはオンデマンドでレンダリングされる必要があります(`output: 'server'`、またはページ側で `export const prerender = false` を指定し、アダプターを併用)。プリレンダリングされたページにはリクエストがないため、アイランドは `{}` を受け取り、ハイドレーション後に URL を読み取ります。
604
+
605
+ ```astro
606
+ ---
607
+ // src/pages/index.astro
608
+ import { Form } from '../components/Form';
609
+ import { Status } from '../components/Status';
610
+
611
+ // サーバーレンダリングが URL と一致するため、ハイドレーションで修正すべきものはありません。
612
+ // プレーンなオブジェクトにします。アイランドの props はシリアライズされますが、URLSearchParams はされません。
613
+ const searchParams = Object.fromEntries(Astro.url.searchParams);
614
+ ---
615
+
616
+ <Form client:load searchParams={searchParams} />
617
+ <Status client:load searchParams={searchParams} />
618
+ ```
619
+
620
+ ```typescript
621
+ // src/components/Form.tsx
622
+ import React from 'react';
623
+ import { useUrlState } from 'state-in-url/astro';
624
+
625
+ import { form } from '../state';
626
+
627
+ export function Form({ searchParams }: { searchParams?: Record<string, string> }) {
628
+ const { urlState, setUrl, setState } = useUrlState(form, { searchParams });
629
+
630
+ const onChangeTags = React.useCallback(
631
+ (tag: (typeof tags)[number]) => {
632
+ setUrl((curr) => ({
633
+ ...curr,
634
+ tags: curr.tags.find((t) => t.id === tag.id)
635
+ ? curr.tags.filter((t) => t.id !== tag.id)
636
+ : curr.tags.concat(tag),
637
+ }));
638
+ },
639
+ [setUrl],
640
+ );
641
+
642
+ return (
643
+ <div>
644
+ {tags.map((tag) => (
645
+ <Tag
646
+ active={!!urlState.tags.find((t) => t.id === tag.id)}
647
+ text={tag.value.text}
648
+ onClick={() => onChangeTags(tag)}
649
+ key={tag.id}
650
+ />
651
+ ))}
652
+
653
+ <input value={urlState.name}
654
+ onChange={(ev) => { setState(curr => ({ ...curr, name: ev.target.value })) }}
655
+ // 状態は即座に更新しつつ、変更は必要に応じて URL に同期できます
656
+ onBlur={() => setUrl()}
657
+ />
658
+ </div>
659
+ );
660
+ }
661
+
662
+ const tags = [
663
+ { id: '1', value: { text: 'React.js', time: new Date('2024-07-17T04:53:17.000Z') } },
664
+ { id: '2', value: { text: 'Next.js', time: new Date('2024-07-18T04:53:17.000Z') } },
665
+ { id: '3', value: { text: 'TailwindCSS', time: new Date('2024-07-19T04:53:17.000Z') } },
666
+ ];
667
+
668
+ // 2つ目のアイランド Status.tsx は同じ状態を読み取ります
669
+ export function Status({ searchParams }: { searchParams?: Record<string, string> }) {
670
+ const { urlState } = useUrlState(form, { searchParams });
671
+ return <pre>{JSON.stringify(urlState, null, 2)}</pre>;
672
+ }
673
+ ```
674
+
675
+ Preact アイランドも同じように動作します。`@astrojs/preact` を `compat: true` で使うと、サーバービルドとクライアントビルドの両方で `react` が `preact/compat` に解決され、上記のインポートはそのまま変わりません。
676
+
677
+ アイランドを使わず、クライアントフレームワークをまったく使わないページでは、同じ状態を [`decodeState` と `encodeState`](#encodestate-と-decodestate-ヘルパー) を通じてフロントマターに置けます:
678
+
679
+ ```astro
680
+ ---
681
+ import { decodeState, encodeState } from 'state-in-url/encodeState';
682
+
683
+ import { form } from '../state';
684
+
685
+ const state = decodeState(Astro.url.searchParams, form);
686
+ const withName = encodeState({ ...state, name: 'Alice' }, form, Astro.url.searchParams);
687
+ ---
688
+
689
+ <pre>{JSON.stringify(state)}</pre>
690
+ <a href={`?${withName}`}>Alice</a>
691
+ ```
692
+
693
+ [サンプルコード](packages/example-astro/src/components/Form-for-test.tsx)、[素の Astro ページ](packages/example-astro/src/pages/pure-astro.astro)
694
+
518
695
  ## レシピ
519
696
  ##### 状態の一部を便利に扱うカスタムフック
520
697
  <details>
@@ -678,7 +855,37 @@ const tags = [
678
855
 
679
856
  ### 他のルーター向け `useUrlStateBase` フック
680
857
 
681
- react-router や tanstack router など、他のルーターで独自の `useUrlState` フックを作成するためのフックです。
858
+ 本パッケージがエントリーポイントを用意していないルーター(例: TanStack Router)向けに、自前の `useUrlState` を作るために使います。`push(url)` と `replace(url)` を持つメモ化されたオブジェクトを渡してください。
859
+
860
+ ルーターが**まったくない**場合は不要です。[`state-in-url/react`](#react-向け-useurlstate-フックルーターなし) を使ってください。こちらは同じフックを `window.history` に接続済みのものです。
861
+
862
+ ```typescript
863
+ import { useUrlStateBase } from 'state-in-url/useUrlStateBase';
864
+
865
+ type FiltersState = { sort: 'name' | 'date'; page: number };
866
+ const FILTERS_STATE: FiltersState = { sort: 'name', page: 1 };
867
+
868
+ export function useFilters() {
869
+ const router = useYourRouter();
870
+ const nav = React.useMemo(
871
+ () => ({
872
+ push: (url: string) => router.navigate(url),
873
+ replace: (url: string) => router.navigate(url, { replace: true }),
874
+ }),
875
+ [router],
876
+ );
877
+
878
+ return useUrlStateBase(FILTERS_STATE, nav, ({ parse }) => parse(window.location.search));
879
+ }
880
+ ```
881
+
882
+ `useUrlState` との違いは 2 点です:
883
+
884
+ - 返すのは `urlState`、`setUrl`、`setState` ではなく `state`、`updateUrl`、`updateState` です。また `updateUrl` の既定は push で、`setUrl` の既定は replace です。
885
+ - 第 3 引数が初期 state を決めます。省略するとフックは読み込み時のクエリ文字列を無視するため、共有されたリンクを開いても既定値が表示されます。
886
+
887
+ > [!NOTE]
888
+ > `useUrlState` はパッケージのルートからはエクスポートされません。どの `useUrlState` もフレームワークに結び付いているため、それぞれ専用のエントリーポイント(`state-in-url/next`、`/react`、`/react-router`、`/react-router6`、`/remix`、`/astro`)にあります。ルートエントリーにはフレームワークに依存しないものだけが残ります: `useSharedState`、`useUrlEncode`、`useUrlStateBase`、`useLinkProps`、`encode`/`decode`、`encodeState`/`decodeState`。
682
889
 
683
890
  [API ドキュメント](packages/urlstate/useUrlStateBase)
684
891
 
@@ -787,7 +994,7 @@ export const useUserState = () => {
787
994
  - [x] `react-router` 向けフック
788
995
  - [x] `remix` 向けフック
789
996
  - [ ] `svelte` 向けフック
790
- - [ ] `astro` 向けフック
997
+ - [x] `astro` 向けフック
791
998
  - [ ] ハッシュに状態を保存するフック?
792
999
 
793
1000
  ## 連絡先とサポート
package/README.ko.md CHANGED
@@ -1,6 +1,6 @@
1
1
  <!-- i18n:start -->
2
2
  [English](./README.md) · [简体中文](./README.zh-CN.md) · [日本語](./README.ja.md) · 한국어 · [Русский](./README.ru.md) · [Español](./README.es.md) · [Português (BR)](./README.pt-BR.md) · [Français](./README.fr.md) · [Tiếng Việt](./README.vi.md)
3
- <!-- i18n:meta locale=ko source=README.md source-blob=bbf45a10aae9e88f68b27d84f51ee3e7e8a1e436 status=translated -->
3
+ <!-- i18n:meta locale=ko source=README.md source-blob=ad78fdbbee096115a953813ed9a462e3393c5736 status=translated -->
4
4
  <!-- i18n:end -->
5
5
 
6
6
  <div align="center">
@@ -69,7 +69,7 @@
69
69
  쿼리 매개변수에 모든 사용자 상태를 저장하세요. 브라우저 URL에 JSON이 있다고 상상해보세요. 데이터의 타입과 구조를 유지하면서 모든 것을 저장할 수 있습니다. 예를 들어 숫자는 문자열이 아닌 숫자로, 날짜는 날짜로 디코딩되며, 객체와 배열도 지원됩니다.
70
70
  매우 간단하고 빠르며 정적 Typescript 유효성 검사를 제공합니다. 딥 링크, 즉 URL 동기화가 쉬워집니다.
71
71
 
72
- Next.js와 react-router용 `useUrlState` 훅과 그 외 JS 환경을 위한 헬퍼를 포함합니다.
72
+ Next.js, react-router, Remix, Astro용 `useUrlState` 훅과 그 외 JS 환경을 위한 헬퍼를 포함합니다.
73
73
  최신 브라우저는 거대한 URL을 지원하고 사용자는 쿼리 문자열을 신경 쓰지 않습니다(전체 선택 후 복사/붙여넣기 워크플로우입니다).
74
74
 
75
75
  쿼리 문자열을 원래 의도대로 상태 관리에 사용할 때입니다.
@@ -98,10 +98,32 @@ Next.js와 react-router용 `useUrlState` 훅과 그 외 JS 환경을 위한 헬
98
98
  - **서버 사이드 렌더링**: 서버 컴포넌트에서 사용 가능, Next.js 14 및 15 지원
99
99
  - **경량**: 의존성 없음, 라이브러리 크기 2KB 미만
100
100
  - **개발자 경험**: 좋은 개발자 경험, 문서, JSDoc 주석 및 예제
101
- - **프레임워크 유연성**: `Next.js` 및 `react-router`용 훅, 다른 프레임워크나 순수 JS와 함께 사용할 수 있는 헬퍼
101
+ - **프레임워크 유연성**: `Next.js`, `react-router`, `Remix`, `Astro`용 훅, 다른 프레임워크나 순수 JS와 함께 사용할 수 있는 헬퍼
102
102
  - **충분한 테스트**: [Chrome/Firefox/Safari용 단위 테스트 및 Playwright 테스트](https://github.com/asmyshlyaev177/state-in-url/actions/workflows/tests.yml)
103
103
  - **자유로운 라이선스**: MIT
104
104
 
105
+ ### nuqs와 비교
106
+
107
+ [nuqs](https://github.com/47ng/nuqs) 대안을 찾고 계신가요? 둘 다 타입이 유지되는 상태를 쿼리 문자열에 저장하지만, 필요한 설정량과 값으로 담을 수 있는 것이 다릅니다.
108
+
109
+ | 항목 | `state-in-url` | `nuqs` |
110
+ | --- | --- | --- |
111
+ | 설정 | 불필요 — hook을 import하면 끝 | 어댑터 컴포넌트로 앱을 감싸야 함 |
112
+ | 상태 형태 | `React.useState` 같은 타입 객체 하나 | 키별 값, 각 키마다 파서 선언 |
113
+ | 컴포넌트 간 재사용 | hook을 한 번 감싸면 — 모든 컴포넌트가 상태 공유, props 불필요 | 파서 맵을 감싸는 hook은 직접 추출 |
114
+ | 중첩 객체와 배열 | 기본 지원 — 구조와 타입 유지 | JSON 파서에 자체 런타임 검증 추가 필요 |
115
+ | 날짜 | 자동으로 유지 | 내장 파서를 키마다 선언 |
116
+ | 크기, 전체 import | 약 2.9 KB gzip | 약 6.7 KB gzip |
117
+ | 런타임 의존성 | 없음 | 1개 |
118
+ | 라우터 | Next.js, React Router v6/v7, Remix, Astro, 순수 JS 헬퍼 | Next.js, React Router, Remix, TanStack Router, 순수 React |
119
+
120
+ 크기: 라이브러리 전체 import, esbuild minify + gzip, 2026년 8월 nuqs 2.10.1 기준 측정.
121
+
122
+ nuqs도 훌륭한 라이브러리입니다. 값마다 읽기 쉬운 쿼리 파라미터를 원하거나 TanStack Router를 쓴다면 nuqs를, 타입 객체 전체를 설정 없이 URL에 넣고 싶다면 state-in-url을 선택하세요.
123
+
124
+
125
+ 전체 비교 — 같은 기능의 두 구현, 다른 대안(TanStack Router, use-query-params), 마이그레이션 노트 — 는 <https://state-in-url.dev/vs/nuqs> 에 있습니다.
126
+
105
127
  ## 목차
106
128
 
107
129
  <!-- toc:start -->
@@ -111,6 +133,7 @@ Next.js와 react-router용 `useUrlState` 훅과 그 외 JS 환경을 위한 헬
111
133
  - [왜 `state-in-url`을 사용해야 하나요?](#왜-state-in-url을-사용해야-하나요)
112
134
  - [사용 사례](#사용-사례)
113
135
  - [특징](#특징)
136
+ - [nuqs와 비교](#nuqs와-비교)
114
137
  - [목차](#목차)
115
138
  - [설치](#설치)
116
139
  - [1. 패키지 설치](#1-패키지-설치)
@@ -127,6 +150,10 @@ Next.js와 react-router용 `useUrlState` 훅과 그 외 JS 환경을 위한 헬
127
150
  - [예제](#예제)
128
151
  - [React-Router용 useUrlState 훅](#react-router용-useurlstate-훅)
129
152
  - [예제](#예제-1)
153
+ - [React용 useUrlState 훅 (라우터 없음)](#react용-useurlstate-훅-라우터-없음)
154
+ - [예제](#예제-2)
155
+ - [Astro용 useUrlState 훅](#astro용-useurlstate-훅)
156
+ - [예제](#예제-3)
130
157
  - [레시피](#레시피)
131
158
  - [상태의 일부를 편리하게 다루기 위한 커스텀 훅](#상태의-일부를-편리하게-다루기-위한-커스텀-훅)
132
159
  - [복잡한 상태 구조 사용](#복잡한-상태-구조-사용)
@@ -511,6 +538,156 @@ const tags = [
511
538
 
512
539
  [예제 코드](packages/example-react-router6/src/Form-for-test.tsx)
513
540
 
541
+ ### React용 useUrlState 훅 (라우터 없음)
542
+
543
+ 라우터가 없는 React 앱을 위한 것입니다. Vite, Create React App, 또는 직접 제어할 수 없는 페이지에 끼워 넣는 위젯이 여기에 해당합니다. 이 훅은 `window.history` 로 URL 을 쓰고, 뒤로/앞으로 이동할 때와 다른 코드가 `pushState`/`replaceState` 를 호출할 때 다시 읽어 옵니다. 같은 state 객체를 쓰는 모든 컴포넌트가 상태를 공유하므로, 감쌀 것도 props 로 넘길 것도 없습니다.
544
+
545
+ [API 문서](packages/urlstate/react/useUrlState)
546
+
547
+ #### 예제
548
+
549
+ ```typescript
550
+ // useFilters.ts
551
+ import { useUrlState } from 'state-in-url/react';
552
+
553
+ type FiltersState = { sort: 'name' | 'date'; page: number };
554
+ const FILTERS_STATE: FiltersState = { sort: 'name', page: 1 };
555
+
556
+ export function useFilters() {
557
+ return useUrlState(FILTERS_STATE);
558
+ }
559
+ ```
560
+
561
+ ```typescript
562
+ // FiltersBar.tsx
563
+ import { useFilters } from './useFilters';
564
+
565
+ export function FiltersBar() {
566
+ const { urlState, setUrl } = useFilters();
567
+
568
+ return <button onClick={() => setUrl({ page: urlState.page + 1 })}>Page {urlState.page}</button>;
569
+ }
570
+ ```
571
+
572
+ `state-in-url/astro` 는 바로 이 훅입니다. islands 는 `searchParams` 를 prop 으로도 받기 때문에 아래에서 따로 설명합니다.
573
+
574
+ ### Astro용 useUrlState 훅
575
+
576
+ React 아일랜드용입니다. Astro에는 기본적으로 클라이언트 측 라우터가 없으므로, 이 훅은 `window.history`로 URL을 쓰고, 뒤로/앞으로 이동과 그 외 모든 `pushState`/`replaceState` 시점에 (Astro 자체의 `<ClientRouter />` 포함) URL을 다시 읽어 옵니다. 한 페이지의 아일랜드들은 감싸는 것 없이 상태를 공유합니다.
577
+
578
+ [API 문서](packages/urlstate/astro/useUrlState)
579
+
580
+ #### 예제
581
+
582
+ ```typescript
583
+ // src/state.ts
584
+ export const form: Form = {
585
+ name: '',
586
+ age: undefined,
587
+ agree_to_terms: false,
588
+ tags: [],
589
+ };
590
+
591
+ type Form = {
592
+ name: string;
593
+ age?: number;
594
+ agree_to_terms: boolean;
595
+ tags: { id: string; value: { text: string; time: Date } }[];
596
+ };
597
+ ```
598
+
599
+ 페이지는 온디맨드로 렌더링되어야 합니다(어댑터와 함께 `output: 'server'`를 쓰거나, 해당 페이지에 `export const prerender = false`를 선언). 프리렌더링된 페이지에는 요청이 없으므로 아일랜드는 `{}`를 받고 하이드레이션 후에 URL을 읽습니다.
600
+
601
+ ```astro
602
+ ---
603
+ // src/pages/index.astro
604
+ import { Form } from '../components/Form';
605
+ import { Status } from '../components/Status';
606
+
607
+ // 서버 렌더링이 URL과 일치하므로 하이드레이션이 바로잡을 것이 없습니다.
608
+ // 일반 객체여야 합니다. 아일랜드 props는 직렬화되지만 URLSearchParams는 직렬화되지 않습니다.
609
+ const searchParams = Object.fromEntries(Astro.url.searchParams);
610
+ ---
611
+
612
+ <Form client:load searchParams={searchParams} />
613
+ <Status client:load searchParams={searchParams} />
614
+ ```
615
+
616
+ ```typescript
617
+ // src/components/Form.tsx
618
+ import React from 'react';
619
+ import { useUrlState } from 'state-in-url/astro';
620
+
621
+ import { form } from '../state';
622
+
623
+ export function Form({ searchParams }: { searchParams?: Record<string, string> }) {
624
+ const { urlState, setUrl, setState } = useUrlState(form, { searchParams });
625
+
626
+ const onChangeTags = React.useCallback(
627
+ (tag: (typeof tags)[number]) => {
628
+ setUrl((curr) => ({
629
+ ...curr,
630
+ tags: curr.tags.find((t) => t.id === tag.id)
631
+ ? curr.tags.filter((t) => t.id !== tag.id)
632
+ : curr.tags.concat(tag),
633
+ }));
634
+ },
635
+ [setUrl],
636
+ );
637
+
638
+ return (
639
+ <div>
640
+ {tags.map((tag) => (
641
+ <Tag
642
+ active={!!urlState.tags.find((t) => t.id === tag.id)}
643
+ text={tag.value.text}
644
+ onClick={() => onChangeTags(tag)}
645
+ key={tag.id}
646
+ />
647
+ ))}
648
+
649
+ <input value={urlState.name}
650
+ onChange={(ev) => { setState(curr => ({ ...curr, name: ev.target.value })) }}
651
+ // 상태를 즉시 업데이트하되 필요에 따라 URL에 동기화
652
+ onBlur={() => setUrl()}
653
+ />
654
+ </div>
655
+ );
656
+ }
657
+
658
+ const tags = [
659
+ { id: '1', value: { text: 'React.js', time: new Date('2024-07-17T04:53:17.000Z') } },
660
+ { id: '2', value: { text: 'Next.js', time: new Date('2024-07-18T04:53:17.000Z') } },
661
+ { id: '3', value: { text: 'TailwindCSS', time: new Date('2024-07-19T04:53:17.000Z') } },
662
+ ];
663
+
664
+ // 두 번째 아일랜드인 Status.tsx는 같은 상태를 읽습니다
665
+ export function Status({ searchParams }: { searchParams?: Record<string, string> }) {
666
+ const { urlState } = useUrlState(form, { searchParams });
667
+ return <pre>{JSON.stringify(urlState, null, 2)}</pre>;
668
+ }
669
+ ```
670
+
671
+ Preact 아일랜드도 같은 방식으로 동작합니다. `@astrojs/preact`와 `compat: true`를 쓰면 서버 빌드와 클라이언트 빌드 모두에서 `react`가 `preact/compat`으로 해석되므로, 위의 import는 그대로입니다.
672
+
673
+ 아일랜드 없이, 클라이언트 프레임워크가 전혀 없는 페이지에서는 같은 상태가 [`decodeState`와 `encodeState`](#encodestate-및-decodestate-헬퍼)를 통해 프론트매터에 존재합니다:
674
+
675
+ ```astro
676
+ ---
677
+ import { decodeState, encodeState } from 'state-in-url/encodeState';
678
+
679
+ import { form } from '../state';
680
+
681
+ const state = decodeState(Astro.url.searchParams, form);
682
+ const withName = encodeState({ ...state, name: 'Alice' }, form, Astro.url.searchParams);
683
+ ---
684
+
685
+ <pre>{JSON.stringify(state)}</pre>
686
+ <a href={`?${withName}`}>Alice</a>
687
+ ```
688
+
689
+ [예제 코드](packages/example-astro/src/components/Form-for-test.tsx), [순수 Astro 페이지](packages/example-astro/src/pages/pure-astro.astro)
690
+
514
691
  ## 레시피
515
692
  ##### 상태의 일부를 편리하게 다루기 위한 커스텀 훅
516
693
  <details>
@@ -674,7 +851,37 @@ const tags = [
674
851
 
675
852
  ### 다른 라우터용 `useUrlStateBase` 훅
676
853
 
677
- 다른 라우터(예: react-router 또는 tanstack router)와 함께 자체 `useUrlState` 훅을 만들기 위한 훅입니다.
854
+ 이 패키지가 진입점을 제공하지 않는 라우터(예: TanStack Router)를 위해 직접 `useUrlState` 를 만들 때 사용합니다. `push(url)` 과 `replace(url)` 을 가진 메모이즈된 객체를 넘기면 됩니다.
855
+
856
+ 라우터가 **전혀 없다면** 이것은 필요 없습니다. 이미 `window.history` 에 연결된 같은 훅인 [`state-in-url/react`](#react용-useurlstate-훅-라우터-없음) 를 쓰세요.
857
+
858
+ ```typescript
859
+ import { useUrlStateBase } from 'state-in-url/useUrlStateBase';
860
+
861
+ type FiltersState = { sort: 'name' | 'date'; page: number };
862
+ const FILTERS_STATE: FiltersState = { sort: 'name', page: 1 };
863
+
864
+ export function useFilters() {
865
+ const router = useYourRouter();
866
+ const nav = React.useMemo(
867
+ () => ({
868
+ push: (url: string) => router.navigate(url),
869
+ replace: (url: string) => router.navigate(url, { replace: true }),
870
+ }),
871
+ [router],
872
+ );
873
+
874
+ return useUrlStateBase(FILTERS_STATE, nav, ({ parse }) => parse(window.location.search));
875
+ }
876
+ ```
877
+
878
+ `useUrlState` 와 다른 점은 두 가지입니다:
879
+
880
+ - `urlState`, `setUrl`, `setState` 가 아니라 `state`, `updateUrl`, `updateState` 를 반환하며, `updateUrl` 의 기본값은 push, `setUrl` 의 기본값은 replace 입니다.
881
+ - 세 번째 인자가 초기 state 를 채웁니다. 이를 생략하면 훅이 페이지가 로드된 시점의 쿼리 문자열을 무시하므로, 공유된 링크를 열어도 기본값만 보입니다.
882
+
883
+ > [!NOTE]
884
+ > `useUrlState` 는 패키지 루트에서 내보내지 않습니다. 모든 `useUrlState` 가 특정 프레임워크에 묶여 있어 각자 자신의 진입점(`state-in-url/next`, `/react`, `/react-router`, `/react-router6`, `/remix`, `/astro`)에 있습니다. 루트 진입점에는 프레임워크와 무관한 것만 남습니다: `useSharedState`, `useUrlEncode`, `useUrlStateBase`, `useLinkProps`, `encode`/`decode`, `encodeState`/`decodeState`.
678
885
 
679
886
  [API 문서](packages/urlstate/useUrlStateBase)
680
887
 
@@ -782,7 +989,7 @@ export const useUserState = () => {
782
989
  - [x] `react-router`용 훅
783
990
  - [x] `remix`용 훅
784
991
  - [ ] `svelte`용 훅
785
- - [ ] `astro`용 훅
992
+ - [x] `astro`용 훅
786
993
  - [ ] 해시에 상태를 저장하는 훅?
787
994
 
788
995
  ## 문의 및 지원