state-in-url 6.3.0 → 6.4.1
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.
- package/README.CN.md +4 -776
- package/README.es.md +836 -0
- package/README.fr.md +836 -0
- package/README.ja.md +836 -0
- package/{README.KO.md → README.ko.md} +69 -8
- package/README.md +34 -3
- package/README.pt-BR.md +836 -0
- package/README.ru.md +836 -0
- package/README.vi.md +836 -0
- package/README.zh-CN.md +836 -0
- package/dist/index.d.ts +9 -8
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/dist/package.json +55 -35
- package/dist/useIsomorphicLayoutEffect.d.ts +2 -0
- package/dist/useIsomorphicLayoutEffect.js +1 -0
- package/dist/useIsomorphicLayoutEffect.mjs +1 -0
- package/dist/useLinkProps/hrefWithState.d.ts +3 -0
- package/dist/useLinkProps/hrefWithState.js +1 -0
- package/dist/useLinkProps/hrefWithState.mjs +1 -0
- package/dist/useLinkProps/index.d.ts +1 -0
- package/dist/useLinkProps/index.js +1 -0
- package/dist/useLinkProps/index.mjs +1 -0
- package/dist/useLinkProps/useLinkProps.d.ts +39 -0
- package/dist/useLinkProps/useLinkProps.js +1 -0
- package/dist/useLinkProps/useLinkProps.mjs +1 -0
- package/dist/useSharedState/useSharedState.js +1 -1
- package/dist/useSharedState/useSharedState.mjs +1 -1
- package/dist/useUrlStateBase/useUrlStateBase.d.ts +3 -3
- package/dist/useUrlStateBase/useUrlStateBase.js +1 -1
- package/dist/useUrlStateBase/useUrlStateBase.mjs +1 -1
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +1 -1
- package/dist/utils.mjs +1 -1
- package/package.json +55 -35
- package/skills/feature-state-hook/SKILL.md +15 -2
- package/skills/form-library-integration/SKILL.md +1 -1
- package/skills/input-handling/SKILL.md +1 -1
- package/skills/nextjs-ssr/SKILL.md +1 -1
- package/skills/react-router-remix-setup/SKILL.md +1 -1
- package/skills/shared-state-no-url/SKILL.md +1 -1
- package/dist/useInsertionEffect.d.ts +0 -2
- package/dist/useInsertionEffect.js +0 -1
- package/dist/useInsertionEffect.mjs +0 -1
package/README.ja.md
ADDED
|
@@ -0,0 +1,836 @@
|
|
|
1
|
+
<!-- i18n:start -->
|
|
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 -->
|
|
4
|
+
<!-- i18n:end -->
|
|
5
|
+
|
|
6
|
+
<div align="center">
|
|
7
|
+
<img src="/packages/example-nextjs14/public/Logo_symbol.png" alt="state-in-url ロゴ" width="120px" />
|
|
8
|
+
|
|
9
|
+
# State in url
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<div align="center">
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
<div align="center">
|
|
16
|
+
|
|
17
|
+
[](https://asmyshlyaev177.dev)
|
|
18
|
+
|
|
19
|
+
[](https://www.npmjs.com/package/state-in-url)
|
|
20
|
+

|
|
21
|
+
[](https://app.codacy.com/gh/asmyshlyaev177/state-in-url/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
|
|
22
|
+
|
|
23
|
+

|
|
24
|
+
[](https://app.codacy.com/gh/asmyshlyaev177/state-in-url/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_coverage)
|
|
25
|
+
[](https://github.com/asmyshlyaev177/state-in-url/)
|
|
26
|
+
[]([https://github.com/semantic-release/semantic-release](https://github.com/asmyshlyaev177/state-in-url))
|
|
27
|
+
|
|
28
|
+
[](https://scorecard.dev/viewer/?uri=github.com/asmyshlyaev177/state-in-url)
|
|
29
|
+
[](https://www.bestpractices.dev/projects/9679)
|
|
30
|
+
[](https://github.com/asmyshlyaev177/state-in-url/blob/master/LICENSE)
|
|
31
|
+
<!-- or by embedding this in your HTML:
|
|
32
|
+
<a href="https://www.bestpractices.dev/projects/9679"><img src="https://www.bestpractices.dev/projects/9679/badge"></a> -->
|
|
33
|
+
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<div align="center">
|
|
37
|
+
|
|
38
|
+
<h4 align="center">バグを発見した場合や、機能のリクエストがあれば、遠慮なく issue を作成してください</h4>
|
|
39
|
+
|
|
40
|
+

|
|
41
|
+
|
|
42
|
+
# デモ
|
|
43
|
+
|
|
44
|
+
<a href="https://state-in-url.dev" target="_blank">デモ</a> |
|
|
45
|
+
<a href="https://state-in-url.netlify.app/" target="_blank">ミラーリンク</a>
|
|
46
|
+
|
|
47
|
+
<a href="https://github.com/asmyshlyaev177/state-in-url/blob/master/Limits.md" target="_blank">URI サイズ制限、<b>最大 12KB</b> までは安全です</a>
|
|
48
|
+
|
|
49
|
+
<hr />
|
|
50
|
+
|
|
51
|
+
プロジェクトを支援するには、<a href="#"><img src="https://raw.githubusercontent.com/acervenky/animated-github-badges/master/assets/starbadge.gif" width="25" height="25"></a> を付けて <a href="https://github.com/asmyshlyaev177" target="_blank">フォロー</a>してください!
|
|
52
|
+
|
|
53
|
+
[ディスカッション](https://github.com/asmyshlyaev177/state-in-url/discussions/1)でフィードバックやご意見をいただけると嬉しいです
|
|
54
|
+
|
|
55
|
+
役に立ったら共有してください。
|
|
56
|
+
[X.com](https://twitter.com/intent/tweet?&url=https://github.com/asmyshlyaev177/state-in-url)
|
|
57
|
+
[LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://github.com/asmyshlyaev177/state-in-url)
|
|
58
|
+
[FB](https://www.facebook.com/sharer.php?u=https://github.com/asmyshlyaev177/state-in-url)
|
|
59
|
+
[VK](http://vk.com/share.php?url=https://github.com/asmyshlyaev177/state-in-url)
|
|
60
|
+
|
|
61
|
+
<hr />
|
|
62
|
+
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
[コードだけ見せて!](#useurlstate)
|
|
66
|
+
|
|
67
|
+
## なぜ `state-in-url` を使うのか?
|
|
68
|
+
|
|
69
|
+
任意のユーザー状態をクエリパラメータに保存します。ブラウザの URL に JSON があるイメージです。データの型と構造を保持したまま、たとえば数値は文字列ではなく数値として、日付は日付としてデコードされ、オブジェクトや配列もサポートされます。
|
|
70
|
+
非常にシンプルで高速、そして静的 TypeScript バリデーション付き。ディープリンク(別名 URL 同期)が簡単になります。
|
|
71
|
+
|
|
72
|
+
Next.js と react-router 用の `useUrlState` フックと、その他あらゆる JS 向けのヘルパーを備えています。
|
|
73
|
+
現代のブラウザは巨大な URL をサポートしており、ユーザーはクエリ文字列を気にしません(全選択してコピー/ペーストするワークフローです)。
|
|
74
|
+
|
|
75
|
+
本来の目的どおりに、クエリ文字列を状態管理に使うときが来ました。
|
|
76
|
+
このライブラリは、そうした退屈な作業をすべて代行します。
|
|
77
|
+
|
|
78
|
+
このライブラリは NUQS の有力な代替になります。
|
|
79
|
+
|
|
80
|
+
### ユースケース
|
|
81
|
+
|
|
82
|
+
- 保存していないユーザーフォームやページフィルターを URL に保存
|
|
83
|
+
- URL と React の状態を同期
|
|
84
|
+
- URI に触れずに、関連のないクライアントコンポーネント間でデータを同期
|
|
85
|
+
- アプリケーション状態を含む共有可能な URL(ディープリンキング、URL 状態同期)
|
|
86
|
+
- ページ再読み込みをまたいだ簡単な状態の永続化
|
|
87
|
+
|
|
88
|
+
### 特徴
|
|
89
|
+
|
|
90
|
+
- 🧩 **シンプル**: プロバイダー、リデューサー、ボイラープレート、新しい概念は不要。`React.useState` に似た API
|
|
91
|
+
- 📘 **TypeScript バリデーション/自動補完**: 状態はただのオブジェクトで、TypeScript 定義に従って IDE/テストで自動的に静的検証されます
|
|
92
|
+
- ✨ **複雑なデータ**: ネストしたオブジェクト、日付、配列。JSON と同じように動き、URL の中に置けます
|
|
93
|
+
- ☂ **デフォルト値**: URL にパラメータがない場合はデフォルト値を提供します
|
|
94
|
+
- ⌨ **整理されている**: 可能な値はすべて最初に定義され、存在しないキーを取得するのを防ぎます
|
|
95
|
+
- **互換性**: サードパーティのクエリパラメータはそのまま保持します
|
|
96
|
+
- **柔軟**: 同じページで複数の状態オブジェクトを使用できます。別のキーを使うだけです
|
|
97
|
+
- **高速**: リレンダーは最小限。大きなオブジェクトのエンコード/デコードは約 [1ms](https://github.com/asmyshlyaev177/state-in-url/blob/87c8c7c995c5cd7d9e7aa039c30bfe64b24abe4b/packages/urlstate/encoder/encoder.test.ts#L185) です
|
|
98
|
+
- **サーバーサイドレンダリング**: サーバーコンポーネントで使用可能。Next.js 14、15、16 をサポート
|
|
99
|
+
- **軽量**: 依存関係ゼロ。ライブラリは 2KB 未満
|
|
100
|
+
- **DX**: 優れた開発者体験。ドキュメント、JSDoc コメント、サンプル付き
|
|
101
|
+
- **フレームワークの柔軟性**: `Next.js` と `react-router` 用のフック、その他のフレームワークやピュア JS で使うためのヘルパー
|
|
102
|
+
- **十分にテスト済み**: [Chrome/Firefox/Safari 向けのユニットテストと Playwright テスト](https://github.com/asmyshlyaev177/state-in-url/actions/workflows/tests.yml)
|
|
103
|
+
- **寛容なライセンス**: MIT
|
|
104
|
+
|
|
105
|
+
## 目次
|
|
106
|
+
|
|
107
|
+
<!-- toc:start -->
|
|
108
|
+
|
|
109
|
+
- [State in url](#state-in-url)
|
|
110
|
+
- [デモ](#デモ)
|
|
111
|
+
- [なぜ `state-in-url` を使うのか?](#なぜ-state-in-url-を使うのか)
|
|
112
|
+
- [ユースケース](#ユースケース)
|
|
113
|
+
- [特徴](#特徴)
|
|
114
|
+
- [目次](#目次)
|
|
115
|
+
- [インストール](#インストール)
|
|
116
|
+
- [1. パッケージのインストール](#1-パッケージのインストール)
|
|
117
|
+
- [2. tsconfig.json の編集](#2-tsconfigjson-の編集)
|
|
118
|
+
- [AI コーディングエージェントと使う](#ai-コーディングエージェントと使う)
|
|
119
|
+
- [useUrlState](#useurlstate)
|
|
120
|
+
- [Next.js 向け useUrlState フック](#nextjs-向け-useurlstate-フック)
|
|
121
|
+
- [使用例](#使用例)
|
|
122
|
+
- [基本](#基本)
|
|
123
|
+
- [サーバーサイドレンダリングで使用](#サーバーサイドレンダリングで使用)
|
|
124
|
+
- [`layout` コンポーネントでフックを使用](#layout-コンポーネントでフックを使用)
|
|
125
|
+
- [任意の状態の形を使用(非推奨)](#任意の状態の形を使用非推奨)
|
|
126
|
+
- [Remix.js 向け useUrlState フック](#remixjs-向け-useurlstate-フック)
|
|
127
|
+
- [例](#例)
|
|
128
|
+
- [React-Router 向け useUrlState フック](#react-router-向け-useurlstate-フック)
|
|
129
|
+
- [例](#例-1)
|
|
130
|
+
- [レシピ](#レシピ)
|
|
131
|
+
- [状態の一部を便利に扱うカスタムフック](#状態の一部を便利に扱うカスタムフック)
|
|
132
|
+
- [複雑な状態の形を使用](#複雑な状態の形を使用)
|
|
133
|
+
- [状態だけを更新して URL には手動で同期](#状態だけを更新して-url-には手動で同期)
|
|
134
|
+
- [その他のフックとヘルパー](#その他のフックとヘルパー)
|
|
135
|
+
- [他のルーター向け `useUrlStateBase` フック](#他のルーター向け-useurlstatebase-フック)
|
|
136
|
+
- [React.js 向け `useSharedState` フック](#reactjs-向け-usesharedstate-フック)
|
|
137
|
+
- [React.js 向け `useLinkProps` フック](#reactjs-向け-uselinkprops-フック)
|
|
138
|
+
- [React.js 向け `useUrlEncode` フック](#reactjs-向け-useurlencode-フック)
|
|
139
|
+
- [`encodeState` と `decodeState` ヘルパー](#encodestate-と-decodestate-ヘルパー)
|
|
140
|
+
- [`encode` と `decode` ヘルパー](#encode-と-decode-ヘルパー)
|
|
141
|
+
- [ベストプラクティス](#ベストプラクティス)
|
|
142
|
+
- [注意点](#注意点)
|
|
143
|
+
- [その他](#その他)
|
|
144
|
+
- [貢献、またはローカルでの実行](#貢献またはローカルでの実行)
|
|
145
|
+
- [ロードマップ](#ロードマップ)
|
|
146
|
+
- [連絡先とサポート](#連絡先とサポート)
|
|
147
|
+
- [変更履歴](#変更履歴)
|
|
148
|
+
- [言及](#言及)
|
|
149
|
+
- [ライセンス](#ライセンス)
|
|
150
|
+
- [採用のご依頼](#採用のご依頼)
|
|
151
|
+
- [インスピレーション](#インスピレーション)
|
|
152
|
+
|
|
153
|
+
<!-- toc:end -->
|
|
154
|
+
|
|
155
|
+
## インストール
|
|
156
|
+
|
|
157
|
+
### 1. パッケージのインストール
|
|
158
|
+
|
|
159
|
+
```sh
|
|
160
|
+
# npm
|
|
161
|
+
npm install --save state-in-url
|
|
162
|
+
# yarn
|
|
163
|
+
yarn add state-in-url
|
|
164
|
+
# pnpm
|
|
165
|
+
pnpm add state-in-url
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### 2. tsconfig.json の編集
|
|
169
|
+
|
|
170
|
+
`tsconfig.json` の `compilerOptions` に `"moduleResolution": "Bundler"`、または `"moduleResolution": "Node16"`、または `"moduleResolution": "NodeNext"` を設定します。
|
|
171
|
+
`"module": "ES2022"` または `"module": "ESNext"` の設定が必要になる場合があります。
|
|
172
|
+
|
|
173
|
+
## AI コーディングエージェントと使う
|
|
174
|
+
|
|
175
|
+
`state-in-url` は [@tanstack/intent](https://tanstack.com/intent/latest/docs/overview) 向けのスキルファイルを同梱しています。AI エージェント(Claude Code、Cursor、Copilot、Codex など)がライブラリ使用時の正しいパターンを読み込み、よくあるミスを避けられます。`state-in-url` をインストールした後、プロジェクトで一度実行してください:
|
|
176
|
+
|
|
177
|
+
```sh
|
|
178
|
+
npx @tanstack/intent@latest install
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
これにより、インストール済みのエージェントが `node_modules/state-in-url/skills/` からスキルを検出するようになります。利用可能なスキルは `npx @tanstack/intent@latest list` で一覧できます。
|
|
182
|
+
|
|
183
|
+
## useUrlState
|
|
184
|
+
|
|
185
|
+
初期状態をパラメータとして受け取り、状態オブジェクト、URL を更新するコールバック、状態だけを更新するコールバックを返すメインフックです。
|
|
186
|
+
同じ `state` オブジェクトを使うすべてのコンポーネントは自動的に同期されます。
|
|
187
|
+
|
|
188
|
+
### Next.js 向け useUrlState フック
|
|
189
|
+
|
|
190
|
+
[完全な API ドキュメント](packages/urlstate/next/useUrlState)
|
|
191
|
+
|
|
192
|
+
[React-Router の例](#react-router-向け-useurlstate-フック)
|
|
193
|
+
|
|
194
|
+
#### 使用例
|
|
195
|
+
|
|
196
|
+
##### 基本
|
|
197
|
+
|
|
198
|
+
1. デフォルト値付きで状態の形を定義します
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
// userState.ts
|
|
202
|
+
// デフォルトと異なる値を持つパラメータだけが URL に入ります。
|
|
203
|
+
export const userState: UserState = { name: '', age: 0 }
|
|
204
|
+
|
|
205
|
+
// `Interface` ではなく `Type` を使うこと!
|
|
206
|
+
type UserState = { name: string, age: number }
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
2. インポートして使用します
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
'use client'
|
|
213
|
+
import { useUrlState } from 'state-in-url/next';
|
|
214
|
+
|
|
215
|
+
import { userState } from './userState';
|
|
216
|
+
|
|
217
|
+
function MyComponent() {
|
|
218
|
+
// `replace` 引数を渡せます。`setUrl` が `router.push` と `router.replace` のどちらを使うかを制御し、デフォルトは replace=true
|
|
219
|
+
// サーバーコンポーネントから `searchParams` を渡せます。サーバーコンポーネントで何かを取得する必要がある場合は `useHistory: false` を渡してください
|
|
220
|
+
const { urlState, setUrl, setState } = useUrlState(userState);
|
|
221
|
+
|
|
222
|
+
return (
|
|
223
|
+
<div>
|
|
224
|
+
// URL が空の場合、urlState.name は `userState` のデフォルト値を返します
|
|
225
|
+
<input value={urlState.name}
|
|
226
|
+
// React.useState と同じ API。例: setUrl(currVal => currVal + 1)
|
|
227
|
+
onChange={(ev) => setUrl({ name: ev.target.value }) }
|
|
228
|
+
/>
|
|
229
|
+
<input value={urlState.age}
|
|
230
|
+
onChange={(ev) => setUrl({ age: +ev.target.value }) }
|
|
231
|
+
/>
|
|
232
|
+
|
|
233
|
+
<input value={urlState.name}
|
|
234
|
+
onChange={(ev) => { setState(curr => ({ ...curr, name: ev.target.value })) }}
|
|
235
|
+
// 状態は即座に更新しつつ、変更は必要に応じて URL に同期できます
|
|
236
|
+
onBlur={() => setUrl()}
|
|
237
|
+
/>
|
|
238
|
+
|
|
239
|
+
<button onClick={() => setUrl((_, initial) => initial)}>
|
|
240
|
+
Reset
|
|
241
|
+
</button>
|
|
242
|
+
|
|
243
|
+
</div>
|
|
244
|
+
)
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
##### サーバーサイドレンダリングで使用
|
|
249
|
+
|
|
250
|
+
<details>
|
|
251
|
+
<Summary>例</Summary>
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
export default async function Home({ searchParams }: { searchParams: object }) {
|
|
255
|
+
return (
|
|
256
|
+
<Form searchParams={searchParams} />
|
|
257
|
+
)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Form.tsx
|
|
261
|
+
'use client'
|
|
262
|
+
import React from 'react';
|
|
263
|
+
import { useUrlState } from 'state-in-url/next';
|
|
264
|
+
import { form } from './form';
|
|
265
|
+
|
|
266
|
+
const Form = ({ searchParams }: { searchParams: object }) => {
|
|
267
|
+
const { urlState, setState, setUrl } = useUrlState(form, { searchParams });
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
</details>
|
|
272
|
+
|
|
273
|
+
##### `layout` コンポーネントでフックを使用
|
|
274
|
+
|
|
275
|
+
<details>
|
|
276
|
+
<Summary>例</Summary>
|
|
277
|
+
これは難しい部分です。app router を使う nextjs はサーバー側から searchParams にアクセスできません。middleware を使う回避策はありますが、美しくはなく、nextjs の更新後に動作しなくなる可能性があります。
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
// 適切な `layout.tsx` に追加
|
|
281
|
+
export const runtime = 'edge';
|
|
282
|
+
|
|
283
|
+
// middleware.ts
|
|
284
|
+
import type { NextRequest } from 'next/server';
|
|
285
|
+
import { NextResponse } from 'next/server';
|
|
286
|
+
|
|
287
|
+
export function middleware(request: NextRequest) {
|
|
288
|
+
const url = request.url?.includes('_next') ? null : request.url;
|
|
289
|
+
const sp = url?.split?.('?')?.[1] || '';
|
|
290
|
+
|
|
291
|
+
const response = NextResponse.next();
|
|
292
|
+
|
|
293
|
+
if (url !== null) {
|
|
294
|
+
response.headers.set('searchParams', sp);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return response;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// 対象の layout コンポーネント
|
|
301
|
+
import { headers } from 'next/headers';
|
|
302
|
+
import { decodeState } from 'state-in-url/encodeState';
|
|
303
|
+
|
|
304
|
+
export default async function Layout({
|
|
305
|
+
children,
|
|
306
|
+
}: {
|
|
307
|
+
children: React.ReactNode;
|
|
308
|
+
}) {
|
|
309
|
+
const sp = headers().get('searchParams') || '';
|
|
310
|
+
|
|
311
|
+
return (
|
|
312
|
+
<div>
|
|
313
|
+
<Comp1 searchParams={decodeState(sp, stateShape)} />
|
|
314
|
+
{children}
|
|
315
|
+
</div>
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
</details>
|
|
323
|
+
|
|
324
|
+
##### 任意の状態の形を使用(非推奨)
|
|
325
|
+
|
|
326
|
+
<details>
|
|
327
|
+
<Summary>例</Summary>
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
'use client'
|
|
331
|
+
import { useUrlState } from 'state-in-url/next';
|
|
332
|
+
|
|
333
|
+
const someObj = {};
|
|
334
|
+
|
|
335
|
+
function SettingsComponent() {
|
|
336
|
+
const { urlState, setUrl, setState } = useUrlState<object>(someObj);
|
|
337
|
+
}
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
</details>
|
|
341
|
+
|
|
342
|
+
### Remix.js 向け useUrlState フック
|
|
343
|
+
|
|
344
|
+
API は Next.js 版と同じですが、[NavigateOptions](https://github.com/remix-run/react-router/blob/bc693ed9f39170bda13b9e1b282fb8e9d5925f66/packages/react-router/lib/context.ts#L99) 型のオプションを渡せます。
|
|
345
|
+
|
|
346
|
+
[API ドキュメント](packages/urlstate/remix/useUrlState)
|
|
347
|
+
|
|
348
|
+
#### 例
|
|
349
|
+
|
|
350
|
+
```typescript
|
|
351
|
+
export const form: Form = {
|
|
352
|
+
name: '',
|
|
353
|
+
age: undefined,
|
|
354
|
+
agree_to_terms: false,
|
|
355
|
+
tags: [],
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
type Form = {
|
|
359
|
+
name: string;
|
|
360
|
+
age?: number;
|
|
361
|
+
agree_to_terms: boolean;
|
|
362
|
+
tags: { id: string; value: { text: string; time: Date } }[];
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
```typescript
|
|
368
|
+
import { useUrlState } from 'state-in-url/remix';
|
|
369
|
+
|
|
370
|
+
import { form } from './form';
|
|
371
|
+
|
|
372
|
+
function TagsComponent() {
|
|
373
|
+
const { urlState, setUrl, setState } = useUrlState(form);
|
|
374
|
+
|
|
375
|
+
const onChangeTags = React.useCallback(
|
|
376
|
+
(tag: (typeof tags)[number]) => {
|
|
377
|
+
setUrl((curr) => ({
|
|
378
|
+
...curr,
|
|
379
|
+
tags: curr.tags.find((t) => t.id === tag.id)
|
|
380
|
+
? curr.tags.filter((t) => t.id !== tag.id)
|
|
381
|
+
: curr.tags.concat(tag),
|
|
382
|
+
}));
|
|
383
|
+
},
|
|
384
|
+
[setUrl],
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
return (
|
|
388
|
+
<div>
|
|
389
|
+
<Field text="Tags">
|
|
390
|
+
<div className="flex flex-wrap gap-2">
|
|
391
|
+
{tags.map((tag) => (
|
|
392
|
+
<Tag
|
|
393
|
+
active={!!urlState.tags.find((t) => t.id === tag.id)}
|
|
394
|
+
text={tag.value.text}
|
|
395
|
+
onClick={() => onChangeTags(tag)}
|
|
396
|
+
key={tag.id}
|
|
397
|
+
/>
|
|
398
|
+
))}
|
|
399
|
+
</div>
|
|
400
|
+
</Field>
|
|
401
|
+
|
|
402
|
+
<input value={urlState.name}
|
|
403
|
+
onChange={(ev) => { setState(curr => ({ ...curr, name: ev.target.value })) }}
|
|
404
|
+
// 状態は即座に更新しつつ、変更は必要に応じて URL に同期できます
|
|
405
|
+
onBlur={() => setUrl()}
|
|
406
|
+
/>
|
|
407
|
+
</div>
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const tags = [
|
|
412
|
+
{
|
|
413
|
+
id: '1',
|
|
414
|
+
value: { text: 'React.js', time: new Date('2024-07-17T04:53:17.000Z') },
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
id: '2',
|
|
418
|
+
value: { text: 'Next.js', time: new Date('2024-07-18T04:53:17.000Z') },
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
id: '3',
|
|
422
|
+
value: { text: 'TailwindCSS', time: new Date('2024-07-19T04:53:17.000Z') },
|
|
423
|
+
},
|
|
424
|
+
];
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
[サンプルコード](packages/example-remix2/app/routes/Form-for-test.tsx)
|
|
428
|
+
|
|
429
|
+
### React-Router 向け useUrlState フック
|
|
430
|
+
|
|
431
|
+
API は Next.js 版と同じですが、[NavigateOptions](https://github.com/remix-run/react-router/blob/bc693ed9f39170bda13b9e1b282fb8e9d5925f66/packages/react-router/lib/context.ts#L99) 型のオプションを渡せます。
|
|
432
|
+
|
|
433
|
+
[API ドキュメント](packages/urlstate/react-router/useUrlState)
|
|
434
|
+
|
|
435
|
+
#### 例
|
|
436
|
+
|
|
437
|
+
```typescript
|
|
438
|
+
export const form: Form = {
|
|
439
|
+
name: '',
|
|
440
|
+
age: undefined,
|
|
441
|
+
agree_to_terms: false,
|
|
442
|
+
tags: [],
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
type Form = {
|
|
446
|
+
name: string;
|
|
447
|
+
age?: number;
|
|
448
|
+
agree_to_terms: boolean;
|
|
449
|
+
tags: { id: string; value: { text: string; time: Date } }[];
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
```typescript
|
|
455
|
+
import { useUrlState } from 'state-in-url/react-router';
|
|
456
|
+
// react-router v6 の場合
|
|
457
|
+
// import { useUrlState } from 'state-in-url/react-router6';
|
|
458
|
+
|
|
459
|
+
import { form } from './form';
|
|
460
|
+
|
|
461
|
+
function TagsComponent() {
|
|
462
|
+
const { urlState, setUrl, setState } = useUrlState(form);
|
|
463
|
+
|
|
464
|
+
const onChangeTags = React.useCallback(
|
|
465
|
+
(tag: (typeof tags)[number]) => {
|
|
466
|
+
setUrl((curr) => ({
|
|
467
|
+
...curr,
|
|
468
|
+
tags: curr.tags.find((t) => t.id === tag.id)
|
|
469
|
+
? curr.tags.filter((t) => t.id !== tag.id)
|
|
470
|
+
: curr.tags.concat(tag),
|
|
471
|
+
}));
|
|
472
|
+
},
|
|
473
|
+
[setUrl],
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
return (
|
|
477
|
+
<div>
|
|
478
|
+
<Field text="Tags">
|
|
479
|
+
<div className="flex flex-wrap gap-2">
|
|
480
|
+
{tags.map((tag) => (
|
|
481
|
+
<Tag
|
|
482
|
+
active={!!urlState.tags.find((t) => t.id === tag.id)}
|
|
483
|
+
text={tag.value.text}
|
|
484
|
+
onClick={() => onChangeTags(tag)}
|
|
485
|
+
key={tag.id}
|
|
486
|
+
/>
|
|
487
|
+
))}
|
|
488
|
+
</div>
|
|
489
|
+
</Field>
|
|
490
|
+
|
|
491
|
+
<input value={urlState.name}
|
|
492
|
+
onChange={(ev) => { setState(curr => ({ ...curr, name: ev.target.value })) }}
|
|
493
|
+
// 状態は即座に更新しつつ、変更は必要に応じて URL に同期できます
|
|
494
|
+
onBlur={() => setUrl()}
|
|
495
|
+
/>
|
|
496
|
+
</div>
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const tags = [
|
|
501
|
+
{
|
|
502
|
+
id: '1',
|
|
503
|
+
value: { text: 'React.js', time: new Date('2024-07-17T04:53:17.000Z') },
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
id: '2',
|
|
507
|
+
value: { text: 'Next.js', time: new Date('2024-07-18T04:53:17.000Z') },
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
id: '3',
|
|
511
|
+
value: { text: 'TailwindCSS', time: new Date('2024-07-19T04:53:17.000Z') },
|
|
512
|
+
},
|
|
513
|
+
];
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
[サンプルコード](packages/example-react-router6/src/Form-for-test.tsx)
|
|
517
|
+
|
|
518
|
+
## レシピ
|
|
519
|
+
##### 状態の一部を便利に扱うカスタムフック
|
|
520
|
+
<details>
|
|
521
|
+
<Summary>例</Summary>
|
|
522
|
+
|
|
523
|
+
```typescript
|
|
524
|
+
'use client';
|
|
525
|
+
|
|
526
|
+
import React from 'react';
|
|
527
|
+
import { useUrlState } from 'state-in-url/next';
|
|
528
|
+
|
|
529
|
+
const form: Form = {
|
|
530
|
+
name: '',
|
|
531
|
+
age: undefined,
|
|
532
|
+
agree_to_terms: false,
|
|
533
|
+
tags: [],
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
type Form = {
|
|
537
|
+
name: string;
|
|
538
|
+
age?: number;
|
|
539
|
+
agree_to_terms: boolean;
|
|
540
|
+
tags: {id: string; value: {text: string; time: Date } }[];
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
export const useFormState = ({ searchParams }: { searchParams?: object }) => {
|
|
544
|
+
const { urlState, setUrl: setUrlBase, reset } = useUrlState(form, {
|
|
545
|
+
searchParams,
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
// 最初のナビゲーションは新しい履歴エントリを push します
|
|
549
|
+
// 以降はすべてそのエントリを replace します
|
|
550
|
+
// これにより履歴は 2 エントリだけになります - ['/url', '/url?key=param']
|
|
551
|
+
|
|
552
|
+
const replace = React.useRef(false);
|
|
553
|
+
const setUrl = React.useCallback((
|
|
554
|
+
state: Parameters<typeof setUrlBase>[0],
|
|
555
|
+
opts?: Parameters<typeof setUrlBase>[1]
|
|
556
|
+
) => {
|
|
557
|
+
setUrlBase(state, { replace: replace.current, ...opts });
|
|
558
|
+
replace.current = true;
|
|
559
|
+
}, [setUrlBase]);
|
|
560
|
+
|
|
561
|
+
return { urlState, setUrl, resetUrl: reset };
|
|
562
|
+
};
|
|
563
|
+
```
|
|
564
|
+
</details>
|
|
565
|
+
|
|
566
|
+
<hr />
|
|
567
|
+
|
|
568
|
+
##### 複雑な状態の形を使用
|
|
569
|
+
|
|
570
|
+
<details>
|
|
571
|
+
<Summary>例</Summary>
|
|
572
|
+
|
|
573
|
+
```typescript
|
|
574
|
+
export const form: Form = {
|
|
575
|
+
name: '',
|
|
576
|
+
age: undefined,
|
|
577
|
+
agree_to_terms: false,
|
|
578
|
+
tags: [],
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
type Form = {
|
|
582
|
+
name: string;
|
|
583
|
+
age?: number;
|
|
584
|
+
agree_to_terms: boolean;
|
|
585
|
+
tags: { id: string; value: { text: string; time: Date } }[];
|
|
586
|
+
};
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
```typescript
|
|
590
|
+
'use client'
|
|
591
|
+
import { useUrlState } from 'state-in-url/next';
|
|
592
|
+
|
|
593
|
+
import { form } from './form';
|
|
594
|
+
|
|
595
|
+
function TagsComponent() {
|
|
596
|
+
// `urlState` は Form 型から推論されます!
|
|
597
|
+
const { urlState, setUrl } = useUrlState(form);
|
|
598
|
+
|
|
599
|
+
const onChangeTags = React.useCallback(
|
|
600
|
+
(tag: (typeof tags)[number]) => {
|
|
601
|
+
setUrl((curr) => ({
|
|
602
|
+
...curr,
|
|
603
|
+
tags: curr.tags.find((t) => t.id === tag.id)
|
|
604
|
+
? curr.tags.filter((t) => t.id !== tag.id)
|
|
605
|
+
: curr.tags.concat(tag),
|
|
606
|
+
}));
|
|
607
|
+
},
|
|
608
|
+
[setUrl],
|
|
609
|
+
);
|
|
610
|
+
|
|
611
|
+
return (
|
|
612
|
+
<div>
|
|
613
|
+
<Field text="Tags">
|
|
614
|
+
<div className="flex flex-wrap gap-2">
|
|
615
|
+
{tags.map((tag) => (
|
|
616
|
+
<Tag
|
|
617
|
+
active={!!urlState.tags.find((t) => t.id === tag.id)}
|
|
618
|
+
text={tag.value.text}
|
|
619
|
+
onClick={() => onChangeTags(tag)}
|
|
620
|
+
key={tag.id}
|
|
621
|
+
/>
|
|
622
|
+
))}
|
|
623
|
+
</div>
|
|
624
|
+
</Field>
|
|
625
|
+
</div>
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const tags = [
|
|
630
|
+
{
|
|
631
|
+
id: '1',
|
|
632
|
+
value: { text: 'React.js', time: new Date('2024-07-17T04:53:17.000Z') },
|
|
633
|
+
},
|
|
634
|
+
{
|
|
635
|
+
id: '2',
|
|
636
|
+
value: { text: 'Next.js', time: new Date('2024-07-18T04:53:17.000Z') },
|
|
637
|
+
},
|
|
638
|
+
{
|
|
639
|
+
id: '3',
|
|
640
|
+
value: { text: 'TailwindCSS', time: new Date('2024-07-19T04:53:17.000Z') },
|
|
641
|
+
},
|
|
642
|
+
];
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
[デモページのサンプルコード](https://github.com/asmyshlyaev177/state-in-url/blob/master/packages/example-nextjs15/src/app/Form.tsx)
|
|
646
|
+
</details>
|
|
647
|
+
|
|
648
|
+
##### 状態だけを更新して URL には手動で同期
|
|
649
|
+
|
|
650
|
+
<details>
|
|
651
|
+
<Summary>例</Summary>
|
|
652
|
+
|
|
653
|
+
```typescript
|
|
654
|
+
|
|
655
|
+
const timer = React.useRef(0 as unknown as NodeJS.Timeout);
|
|
656
|
+
React.useEffect(() => {
|
|
657
|
+
clearTimeout(timer.current);
|
|
658
|
+
timer.current = setTimeout(() => {
|
|
659
|
+
// 参照ではなく内容で状態を比較し、新しい値の場合だけ更新を実行します
|
|
660
|
+
setUrl(urlState);
|
|
661
|
+
}, 500);
|
|
662
|
+
|
|
663
|
+
return () => {
|
|
664
|
+
clearTimeout(timer.current);
|
|
665
|
+
};
|
|
666
|
+
}, [urlState, setUrl]);
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
`onBlur` で状態を同期する方が実際の用途に近いでしょう。
|
|
670
|
+
|
|
671
|
+
```typescript
|
|
672
|
+
<input onBlur={() => updateUrl()} .../>
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
</details>
|
|
676
|
+
|
|
677
|
+
## その他のフックとヘルパー
|
|
678
|
+
|
|
679
|
+
### 他のルーター向け `useUrlStateBase` フック
|
|
680
|
+
|
|
681
|
+
react-router や tanstack router など、他のルーターで独自の `useUrlState` フックを作成するためのフックです。
|
|
682
|
+
|
|
683
|
+
[API ドキュメント](packages/urlstate/useUrlStateBase)
|
|
684
|
+
|
|
685
|
+
### React.js 向け `useSharedState` フック
|
|
686
|
+
|
|
687
|
+
任意の React コンポーネント間で状態を共有するフック。Next.js と Vite でテスト済みです。
|
|
688
|
+
|
|
689
|
+
```typescript
|
|
690
|
+
'use client'
|
|
691
|
+
import { useSharedState } from 'state-in-url';
|
|
692
|
+
|
|
693
|
+
export const someState = { name: '' };
|
|
694
|
+
|
|
695
|
+
function SettingsComponent() {
|
|
696
|
+
const { state, setState } = useSharedState(someState);
|
|
697
|
+
}
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
[API ドキュメント](packages/urlstate/useSharedState/README.ja.md)
|
|
701
|
+
|
|
702
|
+
### React.js 向け `useLinkProps` フック
|
|
703
|
+
|
|
704
|
+
別のルートを指すリンクに状態を引き継ぐためのフックです。言語切り替えなどに使います。`setUrl` は常に現在のパスに書き込みますが、こちらは違います。
|
|
705
|
+
|
|
706
|
+
```tsx
|
|
707
|
+
'use client'
|
|
708
|
+
import Link from 'next/link';
|
|
709
|
+
import { useRouter } from 'next/navigation';
|
|
710
|
+
import { useLinkProps } from 'state-in-url/useLinkProps';
|
|
711
|
+
|
|
712
|
+
export const form = { name: '' };
|
|
713
|
+
|
|
714
|
+
function LanguagePicker() {
|
|
715
|
+
const linkProps = useLinkProps(form, useRouter().push);
|
|
716
|
+
|
|
717
|
+
return <Link {...linkProps('/de/pricing')}>Deutsch</Link>;
|
|
718
|
+
}
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
マークアップには素の `href` が残るため、クローラーと `hreflang` は正規の URL を参照します。状態はクリック時に読み取られます。
|
|
722
|
+
|
|
723
|
+
[API ドキュメント](packages/urlstate/useLinkProps/README.ja.md)
|
|
724
|
+
|
|
725
|
+
### React.js 向け `useUrlEncode` フック
|
|
726
|
+
|
|
727
|
+
[API ドキュメント](packages/urlstate/useUrlEncode/README.ja.md)
|
|
728
|
+
|
|
729
|
+
### `encodeState` と `decodeState` ヘルパー
|
|
730
|
+
|
|
731
|
+
[API ドキュメント](packages/urlstate/encodeState/README.ja.md)
|
|
732
|
+
|
|
733
|
+
### `encode` と `decode` ヘルパー
|
|
734
|
+
|
|
735
|
+
[API ドキュメント](packages/urlstate/encoder/README.ja.md)
|
|
736
|
+
|
|
737
|
+
## ベストプラクティス
|
|
738
|
+
|
|
739
|
+
- 状態の形を定数として定義する
|
|
740
|
+
- 型安全性と自動補完を高めるために TypeScript を使う
|
|
741
|
+
- 機密情報(SSN、API キーなど)を URL パラメータに保存しない
|
|
742
|
+
- 読みやすい TS エラーを得るには、この [拡張機能](https://marketplace.visualstudio.com/items?itemName=yoavbls.pretty-ts-errors) を使う
|
|
743
|
+
|
|
744
|
+
状態の一部に対応する状態フックを作成し、アプリケーション全体で再利用できます。例:
|
|
745
|
+
```Typescript
|
|
746
|
+
type UserState = {
|
|
747
|
+
name: string;
|
|
748
|
+
age: number;
|
|
749
|
+
other: { id: string, value: number }[]
|
|
750
|
+
};
|
|
751
|
+
const userState = {
|
|
752
|
+
name: '',
|
|
753
|
+
age: 0,
|
|
754
|
+
other: [],
|
|
755
|
+
};
|
|
756
|
+
|
|
757
|
+
export const useUserState = () => {
|
|
758
|
+
const { urlState, setUrl, reset } = useUrlState(userState);
|
|
759
|
+
|
|
760
|
+
// その他のロジック
|
|
761
|
+
|
|
762
|
+
// 別のページに移動したときにクエリパラメータをリセット
|
|
763
|
+
React.useEffect(() => {
|
|
764
|
+
return reset
|
|
765
|
+
}, [])
|
|
766
|
+
|
|
767
|
+
return { userState: urlState, setUserState: setUrl };;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
```
|
|
771
|
+
|
|
772
|
+
## 注意点
|
|
773
|
+
|
|
774
|
+
1. シリアライズ可能な値しか渡せません。`Function`、`BigInt`、`Symbol` は動作せず、`ArrayBuffer` のようなものもおそらく動作しません。JSON にシリアライズできるものはすべて動作します。
|
|
775
|
+
2. Vercel サーバーはヘッダー(クエリ文字列など)のサイズを **14KB** に制限しているため、URL の状態は約 5000 語未満に保ってください。<https://vercel.com/docs/errors/URL_TOO_LONG>
|
|
776
|
+
3. app router 付きの `next.js` 14/15/16 でテスト済みです。pages をサポートする予定はありません。
|
|
777
|
+
|
|
778
|
+
## その他
|
|
779
|
+
|
|
780
|
+
### 貢献、またはローカルでの実行
|
|
781
|
+
|
|
782
|
+
[貢献ガイド](CONTRIBUTING.md) を参照
|
|
783
|
+
|
|
784
|
+
## ロードマップ
|
|
785
|
+
|
|
786
|
+
- [x] `Next.js` 向けフック
|
|
787
|
+
- [x] `react-router` 向けフック
|
|
788
|
+
- [x] `remix` 向けフック
|
|
789
|
+
- [ ] `svelte` 向けフック
|
|
790
|
+
- [ ] `astro` 向けフック
|
|
791
|
+
- [ ] ハッシュに状態を保存するフック?
|
|
792
|
+
|
|
793
|
+
## 連絡先とサポート
|
|
794
|
+
|
|
795
|
+
- バグ報告、機能リクエスト、質問には [GitHub issue](https://github.com/asmyshlyaev177/state-in-url/issues) を作成してください
|
|
796
|
+
|
|
797
|
+
## [変更履歴](CHANGELOG.md)
|
|
798
|
+
|
|
799
|
+
## 言及
|
|
800
|
+
|
|
801
|
+
- [This Week in React 209](https://thisweekinreact.com/newsletter/209)
|
|
802
|
+
- [JavaScript Weekly](https://javascriptweekly.com/issues/741)
|
|
803
|
+
- [This Week in React 240](https://thisweekinreact.com/newsletter/240)
|
|
804
|
+
|
|
805
|
+
## ライセンス
|
|
806
|
+
|
|
807
|
+
このプロジェクトは [MIT ライセンス](LICENSE) の下で提供されています。
|
|
808
|
+
|
|
809
|
+
## 採用のご依頼
|
|
810
|
+
|
|
811
|
+
**Aleksandr Smyshliaev** です。このライブラリの唯一の作者でありメンテナです。
|
|
812
|
+
シニアフロントエンドエンジニア(React / Next.js / TypeScript、8 年以上)。現在**フルタイムのリモート勤務を募集しています**。
|
|
813
|
+
|
|
814
|
+
このライブラリは、私の得意分野を凝縮したものです。乱雑なブラウザプリミティブに対する型付き API、依存関係ゼロ、そしていくつもの React メジャーバージョンにわたる Next.js、Remix、React Router での安定性。
|
|
815
|
+
|
|
816
|
+
- **得意分野** — コンポーネントライブラリ、状態管理、リファクタリングに耐えるテストスイート。
|
|
817
|
+
- **その他のプロジェクト** —
|
|
818
|
+
[react-horizontal-scrolling-menu](https://github.com/asmyshlyaev177/react-horizontal-scrolling-menu)
|
|
819
|
+
(週間約 84k インストール)、
|
|
820
|
+
[test-proxy-recorder](https://github.com/asmyshlyaev177/test-proxy-recorder)
|
|
821
|
+
(Playwright 向けの record/replay)、
|
|
822
|
+
[llm-queue](https://github.com/asmyshlyaev177/llm-queue)。
|
|
823
|
+
- **所在地** — ジョージア・トビリシ(GMT+4)。CET と完全に重なります。契約事業体として登録済みなので、B2B 契約に employer-of-record の手配は不要です。
|
|
824
|
+
- **連絡先** — [asmyshlyaev177.dev](https://asmyshlyaev177.dev) ·
|
|
825
|
+
[asmyshlyaev177@gmail.com](mailto:asmyshlyaev177@gmail.com) ·
|
|
826
|
+
[LinkedIn](https://linkedin.com/in/asmyshlyaev177) · Telegram @asmyshlyaev177
|
|
827
|
+
|
|
828
|
+
## インスピレーション
|
|
829
|
+
|
|
830
|
+
[NUQS](https://github.com/47ng/nuqs)
|
|
831
|
+
|
|
832
|
+
[Vue で URL に状態を保存する](https://dev.to/jacobandrewsky/using-url-to-store-state-in-vue-275c)
|
|
833
|
+
|
|
834
|
+
[URL に状態を保存する](https://antonz.org/storing-state/)
|
|
835
|
+
|
|
836
|
+
[NextJS useSearchParams](https://nextjs.org/docs/app/api-reference/functions/use-search-params)
|