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.
Files changed (44) 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/index.d.ts +9 -8
  12. package/dist/index.js +1 -1
  13. package/dist/index.mjs +1 -1
  14. package/dist/package.json +55 -35
  15. package/dist/useIsomorphicLayoutEffect.d.ts +2 -0
  16. package/dist/useIsomorphicLayoutEffect.js +1 -0
  17. package/dist/useIsomorphicLayoutEffect.mjs +1 -0
  18. package/dist/useLinkProps/hrefWithState.d.ts +3 -0
  19. package/dist/useLinkProps/hrefWithState.js +1 -0
  20. package/dist/useLinkProps/hrefWithState.mjs +1 -0
  21. package/dist/useLinkProps/index.d.ts +1 -0
  22. package/dist/useLinkProps/index.js +1 -0
  23. package/dist/useLinkProps/index.mjs +1 -0
  24. package/dist/useLinkProps/useLinkProps.d.ts +39 -0
  25. package/dist/useLinkProps/useLinkProps.js +1 -0
  26. package/dist/useLinkProps/useLinkProps.mjs +1 -0
  27. package/dist/useSharedState/useSharedState.js +1 -1
  28. package/dist/useSharedState/useSharedState.mjs +1 -1
  29. package/dist/useUrlStateBase/useUrlStateBase.d.ts +3 -3
  30. package/dist/useUrlStateBase/useUrlStateBase.js +1 -1
  31. package/dist/useUrlStateBase/useUrlStateBase.mjs +1 -1
  32. package/dist/utils.d.ts +2 -0
  33. package/dist/utils.js +1 -1
  34. package/dist/utils.mjs +1 -1
  35. package/package.json +55 -35
  36. package/skills/feature-state-hook/SKILL.md +15 -2
  37. package/skills/form-library-integration/SKILL.md +1 -1
  38. package/skills/input-handling/SKILL.md +1 -1
  39. package/skills/nextjs-ssr/SKILL.md +1 -1
  40. package/skills/react-router-remix-setup/SKILL.md +1 -1
  41. package/skills/shared-state-no-url/SKILL.md +1 -1
  42. package/dist/useInsertionEffect.d.ts +0 -2
  43. package/dist/useInsertionEffect.js +0 -1
  44. package/dist/useInsertionEffect.mjs +0 -1
package/README.CN.md CHANGED
@@ -1,778 +1,6 @@
1
- # State in url
1
+ # 已移动 / Moved
2
2
 
3
- [English](./README.md) | [한국어](./README.KO.md) | 简体中文
3
+ This file is now **[README.zh-CN.md](./README.zh-CN.md)**, renamed to the
4
+ standard BCP 47 language tag. `CN` is a country code, not a language one.
4
5
 
5
- <div align="center">
6
- <img src="/packages/example-nextjs14/public/Logo_symbol.png" alt="state-in-url logo" width="120px" />
7
-
8
- # State in url
9
- </div>
10
-
11
- <div align="center">
12
- </div>
13
-
14
- <div align="center">
15
-
16
- [![npm](https://img.shields.io/npm/v/state-in-url.svg)](https://www.npmjs.com/package/state-in-url)
17
- ![npm bundle size (minified + gzip)](https://img.shields.io/bundlephobia/minzip/state-in-url.svg)
18
- [![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)
19
-
20
- ![Tests](https://github.com/asmyshlyaev177/state-in-url/actions/workflows/tests.yml/badge.svg?branch=master)
21
- [![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)
22
- [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](https://github.com/asmyshlyaev177/state-in-url/)
23
- [![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))
24
-
25
- [![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)
26
- [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/9679/badge)](https://www.bestpractices.dev/projects/9679)
27
- [![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)
28
-
29
- </div>
30
-
31
- <div align="center">
32
-
33
- <h4 align="center">如果您发现了 bug 或想请求新功能,请随时提交 issue</h4>
34
-
35
- ![Demo-gif](https://github.com/asmyshlyaev177/state-in-url/blob/master/assets/Demo-gif.gif)
36
-
37
- # 演示
38
-
39
- <a href="https://state-in-url.dev" target="_blank">演示</a> |
40
- <a href="https://state-in-url.netlify.app/" target="_blank">镜像链接</a>
41
-
42
- <a href="https://github.com/asmyshlyaev177/state-in-url/blob/master/Limits.md" target="_blank">URI 大小限制,<b>最多 12KB</b> 是安全的</a>
43
-
44
- <hr />
45
-
46
- 添加一个 <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>以支持该项目!
47
-
48
- 非常欢迎您在[讨论区](https://github.com/asmyshlyaev177/state-in-url/discussions/1)分享您的反馈/意见
49
-
50
- 如果对您有用,请分享。
51
- [X.com](https://twitter.com/intent/tweet?&url=https://github.com/asmyshlyaev177/state-in-url)
52
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://github.com/asmyshlyaev177/state-in-url)
53
- [FB](https://www.facebook.com/sharer.php?u=https://github.com/asmyshlyaev177/state-in-url)
54
- [VK](http://vk.com/share.php?url=https://github.com/asmyshlyaev177/state-in-url)
55
-
56
- <hr />
57
-
58
- </div>
59
-
60
- [直接看代码!](#useurlstate)
61
-
62
- ## 为什么使用 `state-in-url`?
63
-
64
- 在查询参数中存储任何用户状态;想象一下在浏览器 URL 中使用 JSON。所有这些都保持数据的类型和结构,例如数字将被解码为数字而不是字符串,日期作为日期,支持对象和数组。
65
- 简单明了、快速,并提供静态 TypeScript 验证。轻松实现深层链接(又名 URL 同步)。
66
-
67
- 包含用于 Next.js 和 react-router 的 `useUrlState` hook,以及用于任何其他 JS 框架的辅助函数。
68
- 由于现代浏览器支持巨大的 URL,用户也不关心查询字符串(这是一个全选和复制/粘贴的工作流程)。
69
-
70
- 是时候使用查询字符串进行状态管理了,正如它最初的用途。
71
- 这个库为您完成所有繁琐的工作。
72
-
73
- 这个库是 NUQS 的一个很好的替代品。
74
-
75
- ### 使用场景
76
-
77
- - 在 URL 中存储未保存的用户表单或页面筛选器
78
- - 将 URL 与 React 状态同步
79
- - 只需在不相关的客户端组件之间同步数据,而无需触及 URI
80
- - 带有应用程序状态的可共享 URL(深层链接、URL 状态同步)
81
- - 轻松实现跨页面重新加载的状态持久化
82
-
83
- ### 特性
84
-
85
- - 🧩 **简单**: 无需 providers、reducers、样板代码或新概念,API 类似于 `React.useState`
86
- - 📘 **TypeScript 验证/自动完成**: 状态只是一个对象,根据 TypeScript 定义在 IDE/测试中自动静态验证
87
- - ✨ **复杂数据**: 嵌套对象、日期和数组,与 JSON 工作方式相同,但在 URL 中
88
- - ☂ **默认值**: 如果参数不在 url 中,为您提供默认值
89
- - ⌨ **有序**: 所有可能的值在开始时定义,防止您获取不存在的键
90
- - **兼容**: 将保持第三方查询参数不变
91
- - **灵活**: 可以在同一页面上使用多个状态对象,只需使用不同的键
92
- - **快速**: 最少的重新渲染,大约[1ms](https://github.com/asmyshlyaev177/state-in-url/blob/87c8c7c995c5cd7d9e7aa039c30bfe64b24abe4b/packages/urlstate/encoder/encoder.test.ts#L185)来编码和解码大对象
93
- - **服务器端渲染**: 可以在服务器组件中使用,支持 Next.js 14 和 15
94
- - **轻量级**: 零依赖,库小于 2KB
95
- - **开发体验**: 良好的开发者体验、文档、JSDoc 注释和示例
96
- - **框架灵活性**: 为 `Next.js` 和 `react-router` 提供 hooks,以及用于其他框架或纯 JS 的辅助函数
97
- - **经过充分测试**: [Chrome/Firefox/Safari 的单元测试和 Playwright 测试](https://github.com/asmyshlyaev177/state-in-url/actions/workflows/tests.yml)
98
- - **宽松许可证**: MIT
99
-
100
- ## 目录
101
-
102
- - [State in url](#state-in-url)
103
- - [State in url](#state-in-url-1)
104
- - [演示](#演示)
105
- - [为什么使用 `state-in-url`?](#为什么使用-state-in-url)
106
- - [使用场景](#使用场景)
107
- - [特性](#特性)
108
- - [目录](#目录)
109
- - [安装](#安装)
110
- - [1. 安装包](#1-安装包)
111
- - [2. 编辑 tsconfig.json](#2-编辑-tsconfigjson)
112
- - [useUrlState](#useurlstate)
113
- - [用于 Next.js 的 useUrlState hook](#用于-nextjs-的-useurlstate-hook)
114
- - [使用示例](#使用示例)
115
- - [基础用法](#基础用法)
116
- - [使用服务器端渲染](#使用服务器端渲染)
117
- - [在 `layout` 组件中使用 hook](#在-layout-组件中使用-hook)
118
- - [使用任意状态形状(不推荐)](#使用任意状态形状不推荐)
119
- - [用于 Remix.js 的 useUrlState hook](#用于-remixjs-的-useurlstate-hook)
120
- - [示例](#示例)
121
- - [用于 React-Router 的 useUrlState hook](#用于-react-router-的-useurlstate-hook)
122
- - [示例](#示例-1)
123
- - [实用技巧](#实用技巧)
124
- - [自定义 hook 以便捷地处理状态切片](#自定义-hook-以便捷地处理状态切片)
125
- - [使用复杂状态形状](#使用复杂状态形状)
126
- - [仅更新状态并手动同步到 URL](#仅更新状态并手动同步到-url)
127
- - [其他 hooks 和辅助函数](#其他-hooks-和辅助函数)
128
- - [`useUrlStateBase` hook 用于其他路由器](#useurlstatebase-hook-用于其他路由器)
129
- - [`useSharedState` hook 用于 React.js](#usesharedstate-hook-用于-reactjs)
130
- - [`useUrlEncode` hook 用于 React.js](#useurlencode-hook-用于-reactjs)
131
- - [`encodeState` 和 `decodeState` 辅助函数](#encodestate-和-decodestate-辅助函数)
132
- - [`encode` 和 `decode` 辅助函数](#encode-和-decode-辅助函数)
133
- - [最佳实践](#最佳实践)
134
- - [注意事项](#注意事项)
135
- - [其他](#其他)
136
- - [贡献和/或本地运行](#贡献和或本地运行)
137
- - [路线图](#路线图)
138
- - [联系和支持](#联系和支持)
139
- - [更新日志](#更新日志)
140
- - [提及](#提及)
141
- - [许可证](#许可证)
142
- - [个人网站](#个人网站)
143
- - [灵感来源](#灵感来源)
144
-
145
- ## 安装
146
-
147
- ### 1. 安装包
148
-
149
- ```sh
150
- # npm
151
- npm install --save state-in-url
152
- # yarn
153
- yarn add state-in-url
154
- # pnpm
155
- pnpm add state-in-url
156
- ```
157
-
158
- ### 2. 编辑 tsconfig.json
159
-
160
- 在 `tsconfig.json` 的 `compilerOptions` 中设置 `"moduleResolution": "Bundler"`,或 `"moduleResolution": "Node16"`,或 `"moduleResolution": "NodeNext"`。
161
- 可能需要设置 `"module": "ES2022"`,或 `"module": "ESNext"`
162
-
163
- ## useUrlState
164
-
165
- 主 hook,接受初始状态作为参数,返回状态对象、更新 url 的回调以及仅更新状态的回调。
166
- 使用相同 `state` 对象的所有组件都会自动同步。
167
-
168
- ### 用于 Next.js 的 useUrlState hook
169
-
170
- [完整 API 文档](packages/urlstate/next/useUrlState)
171
-
172
- [React-Router 示例](#用于-react-router-的-useurlstate-hook)
173
-
174
- #### 使用示例
175
-
176
- ##### 基础用法
177
-
178
- 1. 使用默认值定义状态形状
179
-
180
- ```typescript
181
- // userState.ts
182
- // 只有与默认值不同的参数才会进入 url。
183
- export const userState: UserState = { name: '', age: 0 }
184
-
185
- // 使用 `Type` 而不是 `Interface`!
186
- type UserState = { name: string, age: number }
187
- ```
188
-
189
- 2. 导入并使用
190
-
191
- ```typescript
192
- 'use client'
193
- import { useUrlState } from 'state-in-url/next';
194
-
195
- import { userState } from './userState';
196
-
197
- function MyComponent() {
198
- // 可以传递 `replace` 参数,它控制 `setUrl` 是否使用 `router.push` 或 `router.replace`,默认 replace=true
199
- // 可以从服务器组件传递 `searchParams`,如果需要在服务器组件中获取数据,则传递 `useHistory: false`
200
- const { urlState, setUrl, setState } = useUrlState(userState);
201
-
202
- return (
203
- <div>
204
- // 如果 url 为空,urlState.name 将返回 `userState` 的默认值
205
- <input value={urlState.name}
206
- // 与 React.useState 相同的 API,例如 setUrl(currVal => currVal + 1)
207
- onChange={(ev) => setUrl({ name: ev.target.value }) }
208
- />
209
- <input value={urlState.age}
210
- onChange={(ev) => setUrl({ age: +ev.target.value }) }
211
- />
212
-
213
- <input value={urlState.name}
214
- onChange={(ev) => { setState(curr => ({ ...curr, name: ev.target.value })) }}
215
- // 可以立即更新状态,但根据需要将更改同步到 url
216
- onBlur={() => setUrl()}
217
- />
218
-
219
- <button onClick={() => setUrl((_, initial) => initial)}>
220
- 重置
221
- </button>
222
-
223
- </div>
224
- )
225
- }
226
- ```
227
-
228
- ##### 使用服务器端渲染
229
-
230
- <details>
231
- <Summary>示例</Summary>
232
-
233
- ```typescript
234
- export default async function Home({ searchParams }: { searchParams: object }) {
235
- return (
236
- <Form searchParams={searchParams} />
237
- )
238
- }
239
-
240
- // Form.tsx
241
- 'use client'
242
- import React from 'react';
243
- import { useUrlState } from 'state-in-url/next';
244
- import { form } from './form';
245
-
246
- const Form = ({ searchParams }: { searchParams: object }) => {
247
- const { urlState, setState, setUrl } = useUrlState(form, { searchParams });
248
- }
249
- ```
250
-
251
- </details>
252
-
253
- ##### 在 `layout` 组件中使用 hook
254
-
255
- <details>
256
- <Summary>示例</Summary>
257
- 这是一个棘手的部分,因为带有 app router 的 nextjs 不允许从服务器端访问 searchParams。有一个使用 middleware 的解决方法,但它不够完美,可能在 nextjs 更新后停止工作。
258
-
259
- ```typescript
260
- // 添加到适当的 `layout.tsx`
261
- export const runtime = 'edge';
262
-
263
- // middleware.ts
264
- import type { NextRequest } from 'next/server';
265
- import { NextResponse } from 'next/server';
266
-
267
- export function middleware(request: NextRequest) {
268
- const url = request.url?.includes('_next') ? null : request.url;
269
- const sp = url?.split?.('?')?.[1] || '';
270
-
271
- const response = NextResponse.next();
272
-
273
- if (url !== null) {
274
- response.headers.set('searchParams', sp);
275
- }
276
-
277
- return response;
278
- }
279
-
280
- // 目标布局组件
281
- import { headers } from 'next/headers';
282
- import { decodeState } from 'state-in-url/encodeState';
283
-
284
- export default async function Layout({
285
- children,
286
- }: {
287
- children: React.ReactNode;
288
- }) {
289
- const sp = headers().get('searchParams') || '';
290
-
291
- return (
292
- <div>
293
- <Comp1 searchParams={decodeState(sp, stateShape)} />
294
- {children}
295
- </div>
296
- );
297
- }
298
-
299
-
300
- ```
301
-
302
- </details>
303
-
304
- ##### 使用任意状态形状(不推荐)
305
-
306
- <details>
307
- <Summary>示例</Summary>
308
-
309
- ```typescript
310
- 'use client'
311
- import { useUrlState } from 'state-in-url/next';
312
-
313
- const someObj = {};
314
-
315
- function SettingsComponent() {
316
- const { urlState, setUrl, setState } = useUrlState<object>(someObj);
317
- }
318
- ```
319
-
320
- </details>
321
-
322
- ### 用于 Remix.js 的 useUrlState hook
323
-
324
- API 与 Next.js 版本相同,但可以传递 [NavigateOptions](https://github.com/remix-run/react-router/blob/bc693ed9f39170bda13b9e1b282fb8e9d5925f66/packages/react-router/lib/context.ts#L99) 类型的选项。
325
-
326
- [API 文档](packages/urlstate/remix/useUrlState)
327
-
328
- #### 示例
329
-
330
- ```typescript
331
- export const form: Form = {
332
- name: '',
333
- age: undefined,
334
- agree_to_terms: false,
335
- tags: [],
336
- };
337
-
338
- type Form = {
339
- name: string;
340
- age?: number;
341
- agree_to_terms: boolean;
342
- tags: { id: string; value: { text: string; time: Date } }[];
343
- };
344
-
345
- ```
346
-
347
- ```typescript
348
- import { useUrlState } from 'state-in-url/remix';
349
-
350
- import { form } from './form';
351
-
352
- function TagsComponent() {
353
- const { urlState, setUrl, setState } = useUrlState(form);
354
-
355
- const onChangeTags = React.useCallback(
356
- (tag: (typeof tags)[number]) => {
357
- setUrl((curr) => ({
358
- ...curr,
359
- tags: curr.tags.find((t) => t.id === tag.id)
360
- ? curr.tags.filter((t) => t.id !== tag.id)
361
- : curr.tags.concat(tag),
362
- }));
363
- },
364
- [setUrl],
365
- );
366
-
367
- return (
368
- <div>
369
- <Field text="标签">
370
- <div className="flex flex-wrap gap-2">
371
- {tags.map((tag) => (
372
- <Tag
373
- active={!!urlState.tags.find((t) => t.id === tag.id)}
374
- text={tag.value.text}
375
- onClick={() => onChangeTags(tag)}
376
- key={tag.id}
377
- />
378
- ))}
379
- </div>
380
- </Field>
381
-
382
- <input value={urlState.name}
383
- onChange={(ev) => { setState(curr => ({ ...curr, name: ev.target.value })) }}
384
- // 可以立即更新状态,但根据需要将更改同步到 url
385
- onBlur={() => setUrl()}
386
- />
387
- </div>
388
- );
389
- }
390
-
391
- const tags = [
392
- {
393
- id: '1',
394
- value: { text: 'React.js', time: new Date('2024-07-17T04:53:17.000Z') },
395
- },
396
- {
397
- id: '2',
398
- value: { text: 'Next.js', time: new Date('2024-07-18T04:53:17.000Z') },
399
- },
400
- {
401
- id: '3',
402
- value: { text: 'TailwindCSS', time: new Date('2024-07-19T04:53:17.000Z') },
403
- },
404
- ];
405
- ```
406
-
407
- [示例代码](packages/example-remix2/app/routes/Form-for-test.tsx)
408
-
409
- ### 用于 React-Router 的 useUrlState hook
410
-
411
- API 与 Next.js 版本相同,但可以传递 [NavigateOptions](https://github.com/remix-run/react-router/blob/bc693ed9f39170bda13b9e1b282fb8e9d5925f66/packages/react-router/lib/context.ts#L99) 类型的选项。
412
-
413
- [API 文档](packages/urlstate/react-router/useUrlState)
414
-
415
- #### 示例
416
-
417
- ```typescript
418
- export const form: Form = {
419
- name: '',
420
- age: undefined,
421
- agree_to_terms: false,
422
- tags: [],
423
- };
424
-
425
- type Form = {
426
- name: string;
427
- age?: number;
428
- agree_to_terms: boolean;
429
- tags: { id: string; value: { text: string; time: Date } }[];
430
- };
431
-
432
- ```
433
-
434
- ```typescript
435
- import { useUrlState } from 'state-in-url/react-router';
436
- // 对于 react-router v6
437
- // import { useUrlState } from 'state-in-url/react-router6';
438
-
439
- import { form } from './form';
440
-
441
- function TagsComponent() {
442
- const { urlState, setUrl, setState } = useUrlState(form);
443
-
444
- const onChangeTags = React.useCallback(
445
- (tag: (typeof tags)[number]) => {
446
- setUrl((curr) => ({
447
- ...curr,
448
- tags: curr.tags.find((t) => t.id === tag.id)
449
- ? curr.tags.filter((t) => t.id !== tag.id)
450
- : curr.tags.concat(tag),
451
- }));
452
- },
453
- [setUrl],
454
- );
455
-
456
- return (
457
- <div>
458
- <Field text="标签">
459
- <div className="flex flex-wrap gap-2">
460
- {tags.map((tag) => (
461
- <Tag
462
- active={!!urlState.tags.find((t) => t.id === tag.id)}
463
- text={tag.value.text}
464
- onClick={() => onChangeTags(tag)}
465
- key={tag.id}
466
- />
467
- ))}
468
- </div>
469
- </Field>
470
-
471
- <input value={urlState.name}
472
- onChange={(ev) => { setState(curr => ({ ...curr, name: ev.target.value })) }}
473
- // 可以立即更新状态,但根据需要将更改同步到 url
474
- onBlur={() => setUrl()}
475
- />
476
- </div>
477
- );
478
- }
479
-
480
- const tags = [
481
- {
482
- id: '1',
483
- value: { text: 'React.js', time: new Date('2024-07-17T04:53:17.000Z') },
484
- },
485
- {
486
- id: '2',
487
- value: { text: 'Next.js', time: new Date('2024-07-18T04:53:17.000Z') },
488
- },
489
- {
490
- id: '3',
491
- value: { text: 'TailwindCSS', time: new Date('2024-07-19T04:53:17.000Z') },
492
- },
493
- ];
494
- ```
495
-
496
- [示例代码](packages/example-react-router6/src/Form-for-test.tsx)
497
-
498
- ## 实用技巧
499
- ##### 自定义 hook 以便捷地处理状态切片
500
- <details>
501
- <Summary>示例</Summary>
502
-
503
- ```typescript
504
- 'use client';
505
-
506
- import React from 'react';
507
- import { useUrlState } from 'state-in-url/next';
508
-
509
- const form: Form = {
510
- name: '',
511
- age: undefined,
512
- agree_to_terms: false,
513
- tags: [],
514
- };
515
-
516
- type Form = {
517
- name: string;
518
- age?: number;
519
- agree_to_terms: boolean;
520
- tags: {id: string; value: {text: string; time: Date } }[];
521
- };
522
-
523
- export const useFormState = ({ searchParams }: { searchParams?: object }) => {
524
- const { urlState, setUrl: setUrlBase, reset } = useUrlState(form, {
525
- searchParams,
526
- });
527
-
528
- // 第一次导航将推送新的历史记录条目
529
- // 所有后续操作将仅替换该条目
530
- // 这样历史记录中只有 2 个条目 - ['/url', '/url?key=param']
531
-
532
- const replace = React.useRef(false);
533
- const setUrl = React.useCallback((
534
- state: Parameters<typeof setUrlBase>[0],
535
- opts?: Parameters<typeof setUrlBase>[1]
536
- ) => {
537
- setUrlBase(state, { replace: replace.current, ...opts });
538
- replace.current = true;
539
- }, [setUrlBase]);
540
-
541
- return { urlState, setUrl, resetUrl: reset };
542
- };
543
- ```
544
- </details>
545
-
546
- <hr />
547
-
548
- ##### 使用复杂状态形状
549
-
550
- <details>
551
- <Summary>示例</Summary>
552
-
553
- ```typescript
554
- export const form: Form = {
555
- name: '',
556
- age: undefined,
557
- agree_to_terms: false,
558
- tags: [],
559
- };
560
-
561
- type Form = {
562
- name: string;
563
- age?: number;
564
- agree_to_terms: boolean;
565
- tags: { id: string; value: { text: string; time: Date } }[];
566
- };
567
- ```
568
-
569
- ```typescript
570
- 'use client'
571
- import { useUrlState } from 'state-in-url/next';
572
-
573
- import { form } from './form';
574
-
575
- function TagsComponent() {
576
- // `urlState` 将从 Form 类型推断!
577
- const { urlState, setUrl } = useUrlState(form);
578
-
579
- const onChangeTags = React.useCallback(
580
- (tag: (typeof tags)[number]) => {
581
- setUrl((curr) => ({
582
- ...curr,
583
- tags: curr.tags.find((t) => t.id === tag.id)
584
- ? curr.tags.filter((t) => t.id !== tag.id)
585
- : curr.tags.concat(tag),
586
- }));
587
- },
588
- [setUrl],
589
- );
590
-
591
- return (
592
- <div>
593
- <Field text="标签">
594
- <div className="flex flex-wrap gap-2">
595
- {tags.map((tag) => (
596
- <Tag
597
- active={!!urlState.tags.find((t) => t.id === tag.id)}
598
- text={tag.value.text}
599
- onClick={() => onChangeTags(tag)}
600
- key={tag.id}
601
- />
602
- ))}
603
- </div>
604
- </Field>
605
- </div>
606
- );
607
- }
608
-
609
- const tags = [
610
- {
611
- id: '1',
612
- value: { text: 'React.js', time: new Date('2024-07-17T04:53:17.000Z') },
613
- },
614
- {
615
- id: '2',
616
- value: { text: 'Next.js', time: new Date('2024-07-18T04:53:17.000Z') },
617
- },
618
- {
619
- id: '3',
620
- value: { text: 'TailwindCSS', time: new Date('2024-07-19T04:53:17.000Z') },
621
- },
622
- ];
623
- ```
624
-
625
- [演示页面示例代码](https://github.com/asmyshlyaev177/state-in-url/blob/master/packages/example-nextjs15/src/app/Form.tsx)
626
- </details>
627
-
628
- ##### 仅更新状态并手动同步到 URL
629
-
630
- <details>
631
- <Summary>示例</Summary>
632
-
633
- ```typescript
634
-
635
- const timer = React.useRef(0 as unknown as NodeJS.Timeout);
636
- React.useEffect(() => {
637
- clearTimeout(timer.current);
638
- timer.current = setTimeout(() => {
639
- // 将按内容而不是引用比较状态,仅对新值触发更新
640
- setUrl(urlState);
641
- }, 500);
642
-
643
- return () => {
644
- clearTimeout(timer.current);
645
- };
646
- }, [urlState, setUrl]);
647
- ```
648
-
649
- 在 `onBlur` 时同步状态将更符合实际使用情况。
650
-
651
- ```typescript
652
- <input onBlur={() => updateUrl()} .../>
653
- ```
654
-
655
- </details>
656
-
657
- ## 其他 hooks 和辅助函数
658
-
659
- ### `useUrlStateBase` hook 用于其他路由器
660
-
661
- 用于使用其他路由器(例如 react-router 或 tanstack router)创建自己的 `useUrlState` hooks 的 Hooks。
662
-
663
- [API 文档](packages/urlstate/useUrlStateBase)
664
-
665
- ### `useSharedState` hook 用于 React.js
666
-
667
- 在任何 React 组件之间共享状态的 Hook,已在 Next.js 和 Vite 中测试。
668
-
669
- ```typescript
670
- 'use client'
671
- import { useSharedState } from 'state-in-url';
672
-
673
- export const someState = { name: '' };
674
-
675
- function SettingsComponent() {
676
- const { state, setState } = useSharedState(someState);
677
- }
678
- ```
679
-
680
- [API 文档](packages/urlstate/useSharedState/README.md)
681
-
682
- ### `useUrlEncode` hook 用于 React.js
683
-
684
- [API 文档](packages/urlstate/useUrlEncode/README.md)
685
-
686
- ### `encodeState` 和 `decodeState` 辅助函数
687
-
688
- [API 文档](packages/urlstate/encodeState/README.md)
689
-
690
- ### `encode` 和 `decode` 辅助函数
691
-
692
- [API 文档](packages/urlstate/encoder/README.md)
693
-
694
- ## 最佳实践
695
-
696
- - 将状态形状定义为常量
697
- - 使用 TypeScript 以增强类型安全和自动完成
698
- - 避免在 URL 参数中存储敏感信息(SSN、API 密钥等)
699
- - 使用此[扩展](https://marketplace.visualstudio.com/items?itemName=yoavbls.pretty-ts-errors)以获得可读的 TS 错误
700
-
701
- 可以为状态切片创建状态 hooks,并在整个应用程序中重用它们。例如:
702
- ```Typescript
703
- type UserState = {
704
- name: string;
705
- age: number;
706
- other: { id: string, value: number }[]
707
- };
708
- const userState = {
709
- name: '',
710
- age: 0,
711
- other: [],
712
- };
713
-
714
- export const useUserState = () => {
715
- const { urlState, setUrl, reset } = useUrlState(userState);
716
-
717
- // 其他逻辑
718
-
719
- // 导航到其他页面时重置查询参数
720
- React.useEffect(() => {
721
- return reset
722
- }, [])
723
-
724
- return { userState: urlState, setUserState: setUrl };;
725
- }
726
-
727
- ```
728
-
729
- ## 注意事项
730
-
731
- 1. 只能传递可序列化的值,`Function`、`BigInt` 或 `Symbol` 不起作用,可能像 `ArrayBuffer` 这样的东西也不行。所有可以序列化为 JSON 的内容都可以工作。
732
- 2. Vercel 服务器将标头大小(查询字符串和其他内容)限制为 **14KB**,因此请将 URL 状态保持在约 5000 个字以下。<https://vercel.com/docs/errors/URL_TOO_LONG>
733
- 3. 已在带有 app router 的 `next.js` 14/15 中测试,没有计划支持 pages。
734
-
735
- ## 其他
736
-
737
- ### 贡献和/或本地运行
738
-
739
- 参见[贡献文档](CONTRIBUTING.md)
740
-
741
- ## 路线图
742
-
743
- - [x] 用于 `Next.js` 的 hook
744
- - [x] 用于 `react-router` 的 hook
745
- - [x] 用于 `remix` 的 hook
746
- - [ ] 用于 `svelte` 的 hook
747
- - [ ] 用于 `astro` 的 hook
748
- - [ ] 用于在 hash 中存储状态的 hook ?
749
-
750
- ## 联系和支持
751
-
752
- - 创建 [GitHub issue](https://github.com/asmyshlyaev177/state-in-url/issues) 用于错误报告、功能请求或问题咨询
753
-
754
- ## [更新日志](CHANGELOG.md)
755
-
756
- ## 提及
757
-
758
- - [This Week in React 209](https://thisweekinreact.com/newsletter/209)
759
- - [JavaScript Weekly](https://javascriptweekly.com/issues/741)
760
- - [This Week in React 240](https://thisweekinreact.com/newsletter/240)
761
-
762
- ## 许可证
763
-
764
- 该项目根据 [MIT 许可证](LICENSE)授权。
765
-
766
- ## 个人网站
767
-
768
- [作品集网站](https://asmyshlyaev177.dev)
769
-
770
- ## 灵感来源
771
-
772
- [NUQS](https://github.com/47ng/nuqs)
773
-
774
- [在 Vue 中使用 URL 存储状态](https://dev.to/jacobandrewsky/using-url-to-store-state-in-vue-275c)
775
-
776
- [在 URL 中存储状态](https://antonz.org/storing-state/)
777
-
778
- [NextJS useSearchParams](https://nextjs.org/docs/app/api-reference/functions/use-search-params)
6
+ 本文件已重命名为 **[README.zh-CN.md](./README.zh-CN.md)**。