state-in-url 6.2.4 → 6.4.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 (51) hide show
  1. package/README.CN.md +4 -776
  2. package/README.es.md +836 -0
  3. package/README.fr.md +836 -0
  4. package/README.ja.md +836 -0
  5. package/{README.KO.md → README.ko.md} +69 -8
  6. package/README.md +34 -3
  7. package/README.pt-BR.md +836 -0
  8. package/README.ru.md +836 -0
  9. package/README.vi.md +836 -0
  10. package/README.zh-CN.md +836 -0
  11. package/dist/encoder/encoder.d.ts +1 -1
  12. package/dist/encoder/encoder.js +1 -1
  13. package/dist/encoder/encoder.mjs +1 -1
  14. package/dist/index.d.ts +9 -8
  15. package/dist/index.js +1 -1
  16. package/dist/index.mjs +1 -1
  17. package/dist/next/useUrlState/useUrlState.d.ts +3 -3
  18. package/dist/next/useUrlState/useUrlState.js +1 -1
  19. package/dist/next/useUrlState/useUrlState.mjs +1 -1
  20. package/dist/package.json +34 -13
  21. package/dist/react-router/useUrlState/useUrlState.js +1 -1
  22. package/dist/react-router/useUrlState/useUrlState.mjs +1 -1
  23. package/dist/react-router6/useUrlState/useUrlState.js +1 -1
  24. package/dist/useInsertionEffect.js +1 -1
  25. package/dist/useLinkProps/hrefWithState.d.ts +3 -0
  26. package/dist/useLinkProps/hrefWithState.js +1 -0
  27. package/dist/useLinkProps/hrefWithState.mjs +1 -0
  28. package/dist/useLinkProps/index.d.ts +1 -0
  29. package/dist/useLinkProps/index.js +1 -0
  30. package/dist/useLinkProps/index.mjs +1 -0
  31. package/dist/useLinkProps/useLinkProps.d.ts +39 -0
  32. package/dist/useLinkProps/useLinkProps.js +1 -0
  33. package/dist/useLinkProps/useLinkProps.mjs +1 -0
  34. package/dist/useSharedState/useSharedState.d.ts +1 -1
  35. package/dist/useSharedState/useSharedState.js +1 -1
  36. package/dist/useSharedState/useSharedState.mjs +1 -1
  37. package/dist/useUrlEncode/useUrlEncode.js +1 -1
  38. package/dist/useUrlEncode/useUrlEncode.mjs +1 -1
  39. package/dist/useUrlStateBase/useUrlStateBase.d.ts +3 -3
  40. package/dist/useUrlStateBase/useUrlStateBase.js +1 -1
  41. package/dist/useUrlStateBase/useUrlStateBase.mjs +1 -1
  42. package/dist/utils.d.ts +12 -0
  43. package/dist/utils.js +1 -1
  44. package/dist/utils.mjs +1 -1
  45. package/package.json +34 -13
  46. package/skills/feature-state-hook/SKILL.md +36 -9
  47. package/skills/form-library-integration/SKILL.md +3 -4
  48. package/skills/input-handling/SKILL.md +20 -6
  49. package/skills/nextjs-ssr/SKILL.md +77 -9
  50. package/skills/react-router-remix-setup/SKILL.md +1 -1
  51. package/skills/shared-state-no-url/SKILL.md +1 -1
@@ -0,0 +1,836 @@
1
+ <!-- i18n:start -->
2
+ [English](./README.md) · 简体中文 · [日本語](./README.ja.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=zh-CN 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 logo" width="120px" />
8
+
9
+ # State in url
10
+ </div>
11
+
12
+ <div align="center">
13
+ </div>
14
+
15
+ <div align="center">
16
+
17
+ [![Available for hire](https://img.shields.io/badge/available%20for%20hire-senior%20react%20engineer-2ea44f?style=flat-square)](https://asmyshlyaev177.dev)
18
+
19
+ [![npm](https://img.shields.io/npm/v/state-in-url.svg)](https://www.npmjs.com/package/state-in-url)
20
+ ![npm bundle size (minified + gzip)](https://img.shields.io/bundlephobia/minzip/state-in-url.svg)
21
+ [![Codacy Badge](https://app.codacy.com/project/badge/Grade/73be54068b7f41b0b74a252579ac09ec)](https://app.codacy.com/gh/asmyshlyaev177/state-in-url/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
22
+
23
+ ![Tests](https://github.com/asmyshlyaev177/state-in-url/actions/workflows/tests.yml/badge.svg?branch=master)
24
+ [![Codacy Badge](https://app.codacy.com/project/badge/Coverage/73be54068b7f41b0b74a252579ac09ec)](https://app.codacy.com/gh/asmyshlyaev177/state-in-url/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_coverage)
25
+ [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](https://github.com/asmyshlyaev177/state-in-url/)
26
+ [![semantic-release: angular](https://img.shields.io/badge/semantic--release-angular-e10079?logo=semantic-release)]([https://github.com/semantic-release/semantic-release](https://github.com/asmyshlyaev177/state-in-url))
27
+
28
+ [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/asmyshlyaev177/state-in-url/badge)](https://scorecard.dev/viewer/?uri=github.com/asmyshlyaev177/state-in-url)
29
+ [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/9679/badge)](https://www.bestpractices.dev/projects/9679)
30
+ [![license](https://img.shields.io/github/license/asmyshlyaev177/state-in-url.svg?style=flat-square)](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">如果您发现了 bug 或想请求新功能,请随时提交 issue</h4>
39
+
40
+ ![Demo-gif](https://github.com/asmyshlyaev177/state-in-url/blob/master/assets/Demo-gif.gif)
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` hook,以及用于任何其他 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
+ - 🧩 **简单**: 无需 providers、reducers、样板代码或新概念,API 类似于 `React.useState`
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
99
+ - **轻量级**: 零依赖,库小于 2KB
100
+ - **开发体验**: 良好的开发者体验、文档、JSDoc 注释和示例
101
+ - **框架灵活性**: 为 `Next.js` 和 `react-router` 提供 hooks,以及用于其他框架或纯 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 hook](#用于-nextjs-的-useurlstate-hook)
121
+ - [使用示例](#使用示例)
122
+ - [基础用法](#基础用法)
123
+ - [使用服务器端渲染](#使用服务器端渲染)
124
+ - [在 `layout` 组件中使用 hook](#在-layout-组件中使用-hook)
125
+ - [使用任意状态形状(不推荐)](#使用任意状态形状不推荐)
126
+ - [用于 Remix.js 的 useUrlState hook](#用于-remixjs-的-useurlstate-hook)
127
+ - [示例](#示例)
128
+ - [用于 React-Router 的 useUrlState hook](#用于-react-router-的-useurlstate-hook)
129
+ - [示例](#示例-1)
130
+ - [实用技巧](#实用技巧)
131
+ - [自定义 hook 以便捷地处理状态切片](#自定义-hook-以便捷地处理状态切片)
132
+ - [使用复杂状态形状](#使用复杂状态形状)
133
+ - [仅更新状态并手动同步到 URL](#仅更新状态并手动同步到-url)
134
+ - [其他 hooks 和辅助函数](#其他-hooks-和辅助函数)
135
+ - [`useUrlStateBase` hook 用于其他路由器](#useurlstatebase-hook-用于其他路由器)
136
+ - [`useSharedState` hook 用于 React.js](#usesharedstate-hook-用于-reactjs)
137
+ - [`useLinkProps` hook 用于 React.js](#uselinkprops-hook-用于-reactjs)
138
+ - [`useUrlEncode` hook 用于 React.js](#useurlencode-hook-用于-reactjs)
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
+ 主 hook,接受初始状态作为参数,返回状态对象、更新 url 的回调以及仅更新状态的回调。
186
+ 使用相同 `state` 对象的所有组件都会自动同步。
187
+
188
+ ### 用于 Next.js 的 useUrlState hook
189
+
190
+ [完整 API 文档](packages/urlstate/next/useUrlState)
191
+
192
+ [React-Router 示例](#用于-react-router-的-useurlstate-hook)
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
+ // 使用 `Type` 而不是 `Interface`!
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` 组件中使用 hook
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
+ // 目标布局组件
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 hook
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 hook
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
+ ##### 自定义 hook 以便捷地处理状态切片
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
+ // 第一次导航将推送新的历史记录条目
549
+ // 所有后续操作将仅替换该条目
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
+ ## 其他 hooks 和辅助函数
678
+
679
+ ### `useUrlStateBase` hook 用于其他路由器
680
+
681
+ 用于使用其他路由器(例如 react-router 或 tanstack router)创建自己的 `useUrlState` hooks 的 Hooks。
682
+
683
+ [API 文档](packages/urlstate/useUrlStateBase)
684
+
685
+ ### `useSharedState` hook 用于 React.js
686
+
687
+ 在任何 React 组件之间共享状态的 Hook,已在 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.md)
701
+
702
+ ### `useLinkProps` hook 用于 React.js
703
+
704
+ 用于把状态带到指向另一个路由的链接上的 hook,例如语言切换器。`setUrl` 始终写入当前路径,这个 hook 不会。
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.zh-CN.md)
724
+
725
+ ### `useUrlEncode` hook 用于 React.js
726
+
727
+ [API 文档](packages/urlstate/useUrlEncode/README.md)
728
+
729
+ ### `encodeState` 和 `decodeState` 辅助函数
730
+
731
+ [API 文档](packages/urlstate/encodeState/README.md)
732
+
733
+ ### `encode` 和 `decode` 辅助函数
734
+
735
+ [API 文档](packages/urlstate/encoder/README.md)
736
+
737
+ ## 最佳实践
738
+
739
+ - 将状态形状定义为常量
740
+ - 使用 TypeScript 以增强类型安全和自动完成
741
+ - 避免在 URL 参数中存储敏感信息(SSN、API 密钥等)
742
+ - 使用此[扩展](https://marketplace.visualstudio.com/items?itemName=yoavbls.pretty-ts-errors)以获得可读的 TS 错误
743
+
744
+ 可以为状态切片创建状态 hooks,并在整个应用程序中重用它们。例如:
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 中测试,没有计划支持 pages。
777
+
778
+ ## 其他
779
+
780
+ ### 贡献和/或本地运行
781
+
782
+ 参见[贡献文档](CONTRIBUTING.md)
783
+
784
+ ## 路线图
785
+
786
+ - [x] 用于 `Next.js` 的 hook
787
+ - [x] 用于 `react-router` 的 hook
788
+ - [x] 用于 `remix` 的 hook
789
+ - [ ] 用于 `svelte` 的 hook
790
+ - [ ] 用于 `astro` 的 hook
791
+ - [ ] 用于在 hash 中存储状态的 hook ?
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 的录制/回放)、
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)