state-in-url 6.0.3 → 6.0.5
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 +366 -254
- package/README.KO.md +575 -201
- package/README.md +1 -1
- package/dist/encodeState/encodeState.js +1 -1
- package/dist/encodeState/encodeState.mjs +1 -1
- package/dist/encoder/encoder.js +1 -1
- package/dist/encoder/encoder.mjs +1 -1
- package/dist/subscribers.js +1 -1
- package/dist/subscribers.mjs +1 -1
- package/dist/useUrlEncode/useUrlEncode.js +1 -1
- package/dist/useUrlEncode/useUrlEncode.mjs +1 -1
- package/dist/utils.js +1 -1
- package/dist/utils.mjs +1 -1
- package/package.json +34 -34
- package/dist/encoder/encoder.test.d.ts +0 -1
- package/dist/parseSPObj.test.d.ts +0 -1
- package/dist/useSharedState/useSharedState.test.d.ts +0 -14
- package/dist/useUrlEncode/useUrlEncode.test.d.ts +0 -1
- package/dist/useUrlStateBase/useUrlStateBase.test.d.ts +0 -1
- package/dist/utils.test.d.ts +0 -1
- /package/dist/{encodeState/encodeState.test.d.ts → exportsTest.d.ts} +0 -0
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
|
[](https://scorecard.dev/viewer/?uri=github.com/asmyshlyaev177/state-in-url)
|
|
24
26
|
[](https://www.bestpractices.dev/projects/9679)
|
|
25
27
|
[](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"
|
|
33
|
+
<h4 align="center">如果您发现了 bug 或想请求新功能,请随时提交 issue</h4>
|
|
34
34
|
|
|
35
35
|

|
|
36
36
|
|
|
37
|
-
#
|
|
37
|
+
# 演示
|
|
38
38
|
|
|
39
|
-
<a href="https://state-in-url.dev" target="_blank"
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
[
|
|
60
|
+
[直接看代码!](#useurlstate)
|
|
61
61
|
|
|
62
62
|
## 为什么使用 `state-in-url`?
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
在查询参数中存储任何用户状态;想象一下在浏览器 URL 中使用 JSON。所有这些都保持数据的类型和结构,例如数字将被解码为数字而不是字符串,日期作为日期,支持对象和数组。
|
|
65
|
+
简单明了、快速,并提供静态 TypeScript 验证。轻松实现深层链接(又名 URL 同步)。
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
-
|
|
77
|
+
- 在 URL 中存储未保存的用户表单或页面筛选器
|
|
78
78
|
- 将 URL 与 React 状态同步
|
|
79
|
-
-
|
|
80
|
-
-
|
|
81
|
-
-
|
|
79
|
+
- 只需在不相关的客户端组件之间同步数据,而无需触及 URI
|
|
80
|
+
- 带有应用程序状态的可共享 URL(深层链接、URL 状态同步)
|
|
81
|
+
- 轻松实现跨页面重新加载的状态持久化
|
|
82
82
|
|
|
83
83
|
### 特性
|
|
84
84
|
|
|
85
|
-
- 🧩
|
|
86
|
-
- 📘 **TypeScript
|
|
87
|
-
- ✨
|
|
88
|
-
- ☂
|
|
89
|
-
- ⌨
|
|
90
|
-
-
|
|
91
|
-
-
|
|
92
|
-
-
|
|
93
|
-
-
|
|
94
|
-
-
|
|
95
|
-
-
|
|
96
|
-
-
|
|
97
|
-
-
|
|
98
|
-
-
|
|
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
|
-
- [
|
|
104
|
-
- [
|
|
105
|
-
|
|
106
|
-
- [
|
|
107
|
-
- [
|
|
108
|
-
- [
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
- [
|
|
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. 编辑
|
|
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
|
-
##
|
|
162
|
+
## useUrlState
|
|
145
163
|
|
|
146
|
-
|
|
147
|
-
|
|
164
|
+
主 hook,接受初始状态作为参数,返回状态对象、更新 url 的回调以及仅更新状态的回调。
|
|
165
|
+
使用相同 `state` 对象的所有组件都会自动同步。
|
|
148
166
|
|
|
149
|
-
### Next.js
|
|
167
|
+
### 用于 Next.js 的 useUrlState hook
|
|
150
168
|
|
|
151
169
|
[完整 API 文档](packages/urlstate/next/useUrlState)
|
|
152
170
|
|
|
153
|
-
[React-Router 示例](
|
|
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
|
-
//
|
|
181
|
+
// 只有与默认值不同的参数才会进入 url。
|
|
164
182
|
export const userState: UserState = { name: '', age: 0 }
|
|
165
183
|
|
|
166
|
-
// 使用 `Type`
|
|
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`
|
|
180
|
-
//
|
|
181
|
-
const { urlState, setUrl, setState
|
|
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
|
-
// 如果
|
|
203
|
+
// 如果 url 为空,urlState.name 将返回 `userState` 的默认值
|
|
186
204
|
<input value={urlState.name}
|
|
187
|
-
// 与 React.useState 相同的 API
|
|
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
|
-
//
|
|
214
|
+
// 可以立即更新状态,但根据需要将更改同步到 url
|
|
197
215
|
onBlur={() => setUrl()}
|
|
198
216
|
/>
|
|
199
217
|
|
|
200
|
-
<button onClick={
|
|
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
|
-
|
|
214
|
-
|
|
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
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
tags: [],
|
|
224
|
-
};
|
|
245
|
+
const Form = ({ searchParams }: { searchParams: object }) => {
|
|
246
|
+
const { urlState, setState, setUrl } = useUrlState(form, { searchParams });
|
|
247
|
+
}
|
|
248
|
+
```
|
|
225
249
|
|
|
226
|
-
|
|
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
|
-
|
|
234
|
-
const { urlState, setUrl: setUrlBase, reset } = useUrlState(form, {
|
|
235
|
-
searchParams,
|
|
236
|
-
});
|
|
252
|
+
##### 在 `layout` 组件中使用 hook
|
|
237
253
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
254
|
+
<details>
|
|
255
|
+
<Summary>示例</Summary>
|
|
256
|
+
这是一个棘手的部分,因为带有 app router 的 nextjs 不允许从服务器端访问 searchParams。有一个使用 middleware 的解决方法,但它不够完美,可能在 nextjs 更新后停止工作。
|
|
241
257
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
252
|
-
};
|
|
253
|
-
|
|
254
|
-
</details>
|
|
262
|
+
// middleware.ts
|
|
263
|
+
import type { NextRequest } from 'next/server';
|
|
264
|
+
import { NextResponse } from 'next/server';
|
|
255
265
|
|
|
256
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
[
|
|
336
|
-
</details>
|
|
337
|
-
|
|
338
|
-
##### 仅更新状态并手动同步到 URL
|
|
339
|
-
|
|
340
|
-
<details>
|
|
341
|
-
<Summary>示例</Summary>
|
|
406
|
+
[示例代码](packages/example-remix2/app/routes/Form-for-test.tsx)
|
|
342
407
|
|
|
343
|
-
|
|
408
|
+
### 用于 React-Router 的 useUrlState hook
|
|
344
409
|
|
|
345
|
-
|
|
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
|
-
|
|
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
|
-
|
|
370
|
-
<Summary>示例</Summary>
|
|
414
|
+
#### 示例
|
|
371
415
|
|
|
372
416
|
```typescript
|
|
373
|
-
export
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
417
|
+
export const form: Form = {
|
|
418
|
+
name: '',
|
|
419
|
+
age: undefined,
|
|
420
|
+
agree_to_terms: false,
|
|
421
|
+
tags: [],
|
|
422
|
+
};
|
|
378
423
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
-
|
|
400
|
-
|
|
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
|
-
|
|
417
|
-
}
|
|
438
|
+
import { form } from './form';
|
|
418
439
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
import { decodeState } from 'state-in-url/encodeState';
|
|
440
|
+
function TagsComponent() {
|
|
441
|
+
const { urlState, setUrl, setState } = useUrlState(form);
|
|
422
442
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
-
<
|
|
433
|
-
|
|
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
|
-
|
|
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
|
|
508
|
+
const form: Form = {
|
|
509
|
+
name: '',
|
|
510
|
+
age: undefined,
|
|
511
|
+
agree_to_terms: false,
|
|
512
|
+
tags: [],
|
|
513
|
+
};
|
|
453
514
|
|
|
454
|
-
|
|
455
|
-
|
|
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
|
-
|
|
522
|
+
export const useFormState = ({ searchParams }: { searchParams?: object }) => {
|
|
523
|
+
const { urlState, setUrl: setUrlBase, reset } = useUrlState(form, {
|
|
524
|
+
searchParams,
|
|
525
|
+
});
|
|
460
526
|
|
|
461
|
-
|
|
527
|
+
// 第一次导航将推送新的历史记录条目
|
|
528
|
+
// 所有后续操作将仅替换该条目
|
|
529
|
+
// 这样历史记录中只有 2 个条目 - ['/url', '/url?key=param']
|
|
462
530
|
|
|
463
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
[
|
|
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
|
-
|
|
632
|
+
```typescript
|
|
553
633
|
|
|
554
|
-
|
|
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
|
-
###
|
|
664
|
+
### `useSharedState` hook 用于 React.js
|
|
559
665
|
|
|
560
|
-
|
|
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
|
-
###
|
|
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
|
|
594
|
-
- 使用此
|
|
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.
|
|
627
|
-
2. Vercel
|
|
628
|
-
3.
|
|
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
|
-
参见
|
|
738
|
+
参见[贡献文档](CONTRIBUTING.md)
|
|
637
739
|
|
|
638
740
|
## 路线图
|
|
639
741
|
|
|
640
|
-
- [x] `Next.js`
|
|
641
|
-
- [x] `react-router`
|
|
642
|
-
- [
|
|
643
|
-
- [ ] `svelte`
|
|
644
|
-
- [ ] `astro`
|
|
645
|
-
- [ ]
|
|
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
|
-
|
|
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)
|