state-in-url 6.0.3 → 6.0.4

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