state-in-url 4.2.0 → 4.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.CN.md +454 -176
- package/README.md +2 -0
- package/dist/constants/constants.mjs +1 -1
- package/dist/encodeState/encodeState.mjs +1 -1
- package/dist/encoder/encoder.mjs +1 -1
- package/dist/next/useUrlState/useUrlState.mjs +1 -1
- package/dist/parseSPObj.mjs +1 -1
- package/dist/react-router/useUrlState/useUrlState.mjs +1 -1
- package/dist/subscribers.d.ts +1 -1
- package/dist/subscribers.mjs +1 -1
- package/dist/useSharedState/useSharedState.mjs +1 -1
- package/dist/useUrlEncode/useUrlEncode.d.ts +1 -1
- package/dist/useUrlEncode/useUrlEncode.mjs +1 -1
- package/dist/useUrlStateBase/useUrlStateBase.d.ts +1 -1
- package/dist/useUrlStateBase/useUrlStateBase.mjs +1 -1
- package/dist/utils.d.ts +1 -1
- package/dist/utils.mjs +1 -1
- package/package.json +9 -7
package/README.CN.md
CHANGED
|
@@ -1,74 +1,125 @@
|
|
|
1
1
|
[English](./README.md) | [한국어](./README.KO.md) | 简体中文
|
|
2
2
|
|
|
3
3
|
<div align="center">
|
|
4
|
-
<img src="/
|
|
4
|
+
<img src="/packages/example-nextjs14/public/Logo_symbol.png" alt="state-in-url logo" width="120px" />
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
# State in url
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<div align="center">
|
|
7
10
|
</div>
|
|
8
11
|
|
|
9
12
|
<div align="center">
|
|
10
|
-
<hr />
|
|
11
13
|
|
|
12
14
|
[](https://www.npmjs.com/package/state-in-url)
|
|
13
|
-

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

|
|
19
|
+
[](https://app.codacy.com/gh/asmyshlyaev177/state-in-url/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_coverage)
|
|
15
20
|
[](https://github.com/asmyshlyaev177/state-in-url/)
|
|
16
|
-
]([https://github.com/semantic-release/semantic-release](https://github.com/asmyshlyaev177/state-in-url))
|
|
22
|
+
|
|
23
|
+
[](https://scorecard.dev/viewer/?uri=github.com/asmyshlyaev177/state-in-url)
|
|
24
|
+
[](https://www.bestpractices.dev/projects/9679)
|
|
25
|
+
[](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
|
+
|
|
17
29
|
</div>
|
|
18
30
|
|
|
19
31
|
<div align="center">
|
|
20
32
|
|
|
21
|
-
<h4 align="center"
|
|
33
|
+
<h4 align="center">如果您发现漏洞或有功能需求,请随时提交issue</h4>
|
|
22
34
|
|
|
23
|
-

|
|
24
36
|
|
|
25
|
-
|
|
26
|
-
<a href="https://state-in-url.netlify.app/" target="_blank">DEMO2</a> |
|
|
37
|
+
# 演示页面
|
|
27
38
|
|
|
28
|
-
<
|
|
39
|
+
<a href="https://state-in-url.dev" target="_blank">演示页面</a> |
|
|
40
|
+
<a href="https://state-in-url.netlify.app/" target="_blank">镜像链接</a>
|
|
29
41
|
|
|
30
|
-
|
|
42
|
+
<a href="https://github.com/asmyshlyaev177/state-in-url/blob/master/Limits.md" target="_blank">URI 长度限制: <b>安全上限为 12KB</b> </a>
|
|
31
43
|
|
|
32
44
|
<hr />
|
|
33
45
|
|
|
34
|
-
|
|
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
|
+
非常感激在[discussions](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)
|
|
35
55
|
|
|
36
|
-
|
|
56
|
+
<hr />
|
|
37
57
|
|
|
38
|
-
|
|
58
|
+
</div>
|
|
39
59
|
|
|
40
|
-
#
|
|
60
|
+
[直接给我看代码!](#useurlstate)
|
|
41
61
|
|
|
42
|
-
|
|
43
|
-
- 🔗 包含完整应用程序状态的可共享URL
|
|
44
|
-
- 🔄 在页面重新加载之间轻松持久化状态
|
|
45
|
-
- 🧠 在不相关的客户端组件之间同步数据
|
|
46
|
-
- 🧮 在URL中存储未保存的用户表单
|
|
62
|
+
## 为什么使用 `state-in-url`?
|
|
47
63
|
|
|
48
|
-
|
|
64
|
+
将任何用户状态存储在查询参数中;想象一下在浏览器 URL 中嵌入 JSON 数据,同时保持数据的类型和结构。例如数字会被解码为数字而不是字符串,日期会被解码为日期,支持对象和数组等复杂结构。
|
|
65
|
+
简单、快速,并且支持静态 TypeScript 验证。深度链接(即 URL 同步)变得轻而易举。
|
|
49
66
|
|
|
50
|
-
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
67
|
+
提供了适用于 Next.js 和 react-router 的 `useUrlState` 钩子,以及适用于其他 JavaScript 场景的辅助工具。
|
|
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
|
+
- **服务端渲染支持**:可在 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 许可证,可自由使用。
|
|
56
99
|
|
|
57
100
|
## 目录
|
|
58
101
|
|
|
59
|
-
- [安装](#安装)
|
|
60
|
-
- [
|
|
61
|
-
- [
|
|
62
|
-
- [React
|
|
63
|
-
- [
|
|
64
|
-
- [
|
|
65
|
-
- [
|
|
66
|
-
- [
|
|
67
|
-
- [
|
|
68
|
-
- [
|
|
69
|
-
- [
|
|
70
|
-
- [
|
|
71
|
-
- [
|
|
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
|
+
---
|
|
72
123
|
|
|
73
124
|
## 安装
|
|
74
125
|
|
|
@@ -83,29 +134,38 @@ yarn add state-in-url
|
|
|
83
134
|
pnpm add state-in-url
|
|
84
135
|
```
|
|
85
136
|
|
|
86
|
-
### 2. 编辑tsconfig.json
|
|
137
|
+
### 2. 编辑 `tsconfig.json`
|
|
138
|
+
|
|
139
|
+
在 `tsconfig.json` 的 `compilerOptions` 中设置 `"moduleResolution": "Bundler"`,或 `"moduleResolution": "Node16"`,或 `"moduleResolution": "NodeNext"`。
|
|
140
|
+
可能还需要设置 `"module": "ES2022"` 或 `"module": "ESNext"`。
|
|
141
|
+
|
|
142
|
+
---
|
|
87
143
|
|
|
88
|
-
|
|
144
|
+
## `useUrlState`
|
|
89
145
|
|
|
90
|
-
|
|
146
|
+
主钩子,接收初始状态作为参数,返回状态对象、更新 URL 的回调函数以及仅更新状态的回调函数。
|
|
147
|
+
所有使用相同 `state` 对象的组件会自动同步。
|
|
91
148
|
|
|
92
|
-
|
|
149
|
+
### Next.js 专用的 `useUrlState` 钩子
|
|
93
150
|
|
|
94
|
-
|
|
151
|
+
[完整 API 文档](packages/urlstate/next/useUrlState)
|
|
95
152
|
|
|
96
|
-
|
|
153
|
+
[React-Router 示例](#React-Router-专用的-useUrlState-钩子)
|
|
97
154
|
|
|
98
|
-
####
|
|
155
|
+
#### 使用示例
|
|
99
156
|
|
|
100
|
-
|
|
157
|
+
##### 基础用法
|
|
101
158
|
|
|
102
|
-
|
|
103
|
-
// countState.ts
|
|
104
|
-
// 状态形状应该存储在一个常量中,不要直接传递对象
|
|
105
|
-
export const countState: CountState = { count: 0 }
|
|
159
|
+
1. 定义状态结构及默认值
|
|
106
160
|
|
|
107
|
-
|
|
108
|
-
|
|
161
|
+
```typescript
|
|
162
|
+
// userState.ts
|
|
163
|
+
// 只有与默认值不同的参数会被放入 URL。
|
|
164
|
+
export const userState: UserState = { name: '', age: 0 }
|
|
165
|
+
|
|
166
|
+
// 使用 `Type` 而非 `Interface`!
|
|
167
|
+
type UserState = { name: string, age: number }
|
|
168
|
+
```
|
|
109
169
|
|
|
110
170
|
2. 导入并使用
|
|
111
171
|
|
|
@@ -113,136 +173,206 @@ pnpm add state-in-url
|
|
|
113
173
|
'use client'
|
|
114
174
|
import { useUrlState } from 'state-in-url/next';
|
|
115
175
|
|
|
116
|
-
import {
|
|
176
|
+
import { userState } from './userState';
|
|
117
177
|
|
|
118
178
|
function MyComponent() {
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
const { state, updateState, updateUrl } = useUrlState(countState);
|
|
123
|
-
|
|
124
|
-
// 不允许你意外地直接修改状态,需要TS
|
|
125
|
-
// state.count = 2 // <- 错误
|
|
179
|
+
// 可以传递 `replace` 参数,控制 `setUrl` 使用 `router.push` 还是 `router.replace`,默认为 replace=true
|
|
180
|
+
// 可以传递 `searchParams` 从服务端组件传入,如果需要从服务端组件获取数据,传递 `useHistory: false`
|
|
181
|
+
const { urlState, setUrl, setState, reset } = useUrlState(userState);
|
|
126
182
|
|
|
127
183
|
return (
|
|
128
184
|
<div>
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
<button onClick={
|
|
185
|
+
// 如果 URL 为空,urlState.name 将返回 `userState` 的默认值
|
|
186
|
+
<input value={urlState.name}
|
|
187
|
+
// 与 React.useState 相同的 API,例如 setUrl(currVal => currVal + 1)
|
|
188
|
+
onChange={(ev) => setUrl({ name: ev.target.value }) }
|
|
189
|
+
/>
|
|
190
|
+
<input value={urlState.age}
|
|
191
|
+
onChange={(ev) => setUrl({ age: +ev.target.value }) }
|
|
192
|
+
/>
|
|
193
|
+
|
|
194
|
+
<input value={urlState.name}
|
|
195
|
+
onChange={(ev) => { setState(curr => ({ ...curr, name: ev.target.value })) }}
|
|
196
|
+
// 可以立即更新状态,但根据需要同步到 URL
|
|
197
|
+
onBlur={() => setUrl()}
|
|
198
|
+
/>
|
|
199
|
+
|
|
200
|
+
<button onClick={reset}>
|
|
145
201
|
重置
|
|
146
202
|
</button>
|
|
203
|
+
|
|
147
204
|
</div>
|
|
148
205
|
)
|
|
149
206
|
}
|
|
150
207
|
```
|
|
151
208
|
|
|
152
|
-
|
|
209
|
+
##### 自定义钩子以方便处理状态片段
|
|
210
|
+
<details>
|
|
211
|
+
<Summary>示例</Summary>
|
|
153
212
|
|
|
154
|
-
```typescript
|
|
155
|
-
|
|
156
|
-
theme: 'light' | 'dark';
|
|
157
|
-
fontSize: number;
|
|
158
|
-
notifications: boolean;
|
|
159
|
-
}
|
|
213
|
+
```typescript
|
|
214
|
+
'use client';
|
|
160
215
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
216
|
+
import React from 'react';
|
|
217
|
+
import { useUrlState } from 'state-in-url/next';
|
|
218
|
+
|
|
219
|
+
const form: Form = {
|
|
220
|
+
name: '',
|
|
221
|
+
age: undefined,
|
|
222
|
+
agree_to_terms: false,
|
|
223
|
+
tags: [],
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
type Form = {
|
|
227
|
+
name: string;
|
|
228
|
+
age?: number;
|
|
229
|
+
agree_to_terms: boolean;
|
|
230
|
+
tags: {id: string; value: {text: string; time: Date } }[];
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
export const useFormState = ({ searchParams }: { searchParams?: object }) => {
|
|
234
|
+
const { urlState, setUrl: setUrlBase, reset } = useUrlState(form, {
|
|
235
|
+
searchParams,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// 第一次导航会推送新的历史记录
|
|
239
|
+
// 后续导航将替换该记录
|
|
240
|
+
// 这样历史记录中只有两个条目 - ['/url', '/url?key=param']
|
|
241
|
+
|
|
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]);
|
|
250
|
+
|
|
251
|
+
return { urlState, setUrl, resetUrl: reset };
|
|
252
|
+
};
|
|
253
|
+
```
|
|
254
|
+
</details>
|
|
255
|
+
|
|
256
|
+
<hr />
|
|
257
|
+
|
|
258
|
+
##### 复杂状态结构
|
|
259
|
+
|
|
260
|
+
<details>
|
|
261
|
+
<Summary>示例</Summary>
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
export const form: Form = {
|
|
265
|
+
name: '',
|
|
266
|
+
age: undefined,
|
|
267
|
+
agree_to_terms: false,
|
|
268
|
+
tags: [],
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
type Form = {
|
|
272
|
+
name: string;
|
|
273
|
+
age?: number;
|
|
274
|
+
agree_to_terms: boolean;
|
|
275
|
+
tags: { id: string; value: { text: string; time: Date } }[];
|
|
276
|
+
};
|
|
166
277
|
```
|
|
167
278
|
|
|
168
279
|
```typescript
|
|
169
280
|
'use client'
|
|
170
281
|
import { useUrlState } from 'state-in-url/next';
|
|
171
282
|
|
|
172
|
-
import {
|
|
173
|
-
|
|
174
|
-
function SettingsComponent() {
|
|
175
|
-
// `state` 将从UserSettings类型推断!
|
|
176
|
-
const { state, updateUrl } = useUrlState(userSettings);
|
|
177
|
-
|
|
178
|
-
const toggleTheme = () => {
|
|
179
|
-
updateUrl(current => ({
|
|
180
|
-
...current,
|
|
181
|
-
theme: current.theme === 'light' ? 'dark' : 'light',
|
|
182
|
-
}));
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
// 空闲时同步状态到URL
|
|
186
|
-
const timer = React.useRef(0 as unknown as NodeJS.Timeout);
|
|
187
|
-
React.useEffect(() => {
|
|
188
|
-
clearTimeout(timer.current);
|
|
189
|
-
timer.current = setTimeout(() => {
|
|
190
|
-
// 将通过内容而不是引用比较状态,只为新值触发更新
|
|
191
|
-
updateUrl(state);
|
|
192
|
-
}, 500);
|
|
283
|
+
import { form } from './form';
|
|
193
284
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
285
|
+
function TagsComponent() {
|
|
286
|
+
// `urlState` 会从 Form 类型推断!
|
|
287
|
+
const { urlState, setUrl } = useUrlState(form);
|
|
288
|
+
|
|
289
|
+
const onChangeTags = React.useCallback(
|
|
290
|
+
(tag: (typeof tags)[number]) => {
|
|
291
|
+
setUrl((curr) => ({
|
|
292
|
+
...curr,
|
|
293
|
+
tags: curr.tags.find((t) => t.id === tag.id)
|
|
294
|
+
? curr.tags.filter((t) => t.id !== tag.id)
|
|
295
|
+
: curr.tags.concat(tag),
|
|
296
|
+
}));
|
|
297
|
+
},
|
|
298
|
+
[setUrl],
|
|
299
|
+
);
|
|
198
300
|
|
|
199
301
|
return (
|
|
200
302
|
<div>
|
|
201
|
-
<
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
303
|
+
<Field text="标签">
|
|
304
|
+
<div className="flex flex-wrap gap-2">
|
|
305
|
+
{tags.map((tag) => (
|
|
306
|
+
<Tag
|
|
307
|
+
active={!!urlState.tags.find((t) => t.id === tag.id)}
|
|
308
|
+
text={tag.value.text}
|
|
309
|
+
onClick={() => onChangeTags(tag)}
|
|
310
|
+
key={tag.id}
|
|
311
|
+
/>
|
|
312
|
+
))}
|
|
313
|
+
</div>
|
|
314
|
+
</Field>
|
|
206
315
|
</div>
|
|
207
316
|
);
|
|
208
317
|
}
|
|
209
|
-
...
|
|
210
|
-
|
|
211
|
-
// 其他组件
|
|
212
|
-
function Component() {
|
|
213
|
-
const { state } = useUrlState(defaultSettings);
|
|
214
318
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
319
|
+
const tags = [
|
|
320
|
+
{
|
|
321
|
+
id: '1',
|
|
322
|
+
value: { text: 'React.js', time: new Date('2024-07-17T04:53:17.000Z') },
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
id: '2',
|
|
326
|
+
value: { text: 'Next.js', time: new Date('2024-07-18T04:53:17.000Z') },
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
id: '3',
|
|
330
|
+
value: { text: 'TailwindCSS', time: new Date('2024-07-19T04:53:17.000Z') },
|
|
331
|
+
},
|
|
332
|
+
];
|
|
221
333
|
```
|
|
222
334
|
|
|
223
|
-
|
|
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>
|
|
342
|
+
|
|
343
|
+
```typescript
|
|
224
344
|
|
|
225
|
-
```typescript
|
|
226
345
|
const timer = React.useRef(0 as unknown as NodeJS.Timeout);
|
|
227
346
|
React.useEffect(() => {
|
|
228
347
|
clearTimeout(timer.current);
|
|
229
348
|
timer.current = setTimeout(() => {
|
|
230
|
-
//
|
|
231
|
-
|
|
349
|
+
// 会比较状态内容而非引用,仅对新值触发更新
|
|
350
|
+
setUrl(urlState);
|
|
232
351
|
}, 500);
|
|
233
352
|
|
|
234
353
|
return () => {
|
|
235
354
|
clearTimeout(timer.current);
|
|
236
355
|
};
|
|
237
|
-
}, [
|
|
356
|
+
}, [urlState, setUrl]);
|
|
238
357
|
```
|
|
239
358
|
|
|
240
|
-
|
|
359
|
+
在 `onBlur` 时同步状态更符合实际使用场景。
|
|
360
|
+
|
|
361
|
+
```typescript
|
|
362
|
+
<input onBlur={() => updateUrl()} .../>
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
</details>
|
|
366
|
+
|
|
367
|
+
##### 服务端渲染
|
|
368
|
+
|
|
369
|
+
<details>
|
|
370
|
+
<Summary>示例</Summary>
|
|
241
371
|
|
|
242
372
|
```typescript
|
|
243
373
|
export default async function Home({ searchParams }: { searchParams: object }) {
|
|
244
374
|
return (
|
|
245
|
-
<Form
|
|
375
|
+
<Form searchParams={searchParams} />
|
|
246
376
|
)
|
|
247
377
|
}
|
|
248
378
|
|
|
@@ -252,17 +382,21 @@ import React from 'react';
|
|
|
252
382
|
import { useUrlState } from 'state-in-url/next';
|
|
253
383
|
import { form } from './form';
|
|
254
384
|
|
|
255
|
-
const Form = ({
|
|
256
|
-
const {
|
|
385
|
+
const Form = ({ searchParams }: { searchParams: object }) => {
|
|
386
|
+
const { urlState, setState, setUrl } = useUrlState(form, { searchParams });
|
|
257
387
|
}
|
|
258
388
|
```
|
|
259
389
|
|
|
260
|
-
|
|
390
|
+
</details>
|
|
391
|
+
|
|
392
|
+
##### 在 `layout` 组件中使用钩子
|
|
261
393
|
|
|
262
|
-
|
|
394
|
+
<details>
|
|
395
|
+
<Summary>示例</Summary>
|
|
396
|
+
这是一个棘手的部分,因为 Next.js 的 app router 不允许从服务端访问 searchParams。可以使用中间件解决,但不够优雅,且可能在 Next.js 更新后失效。
|
|
263
397
|
|
|
264
398
|
```typescript
|
|
265
|
-
//
|
|
399
|
+
// 添加到适当的 `layout.tsc`
|
|
266
400
|
export const runtime = 'edge';
|
|
267
401
|
|
|
268
402
|
// middleware.ts
|
|
@@ -282,7 +416,7 @@ export function middleware(request: NextRequest) {
|
|
|
282
416
|
return response;
|
|
283
417
|
}
|
|
284
418
|
|
|
285
|
-
//
|
|
419
|
+
// 目标布局组件
|
|
286
420
|
import { headers } from 'next/headers';
|
|
287
421
|
import { decodeState } from 'state-in-url/encodeState';
|
|
288
422
|
|
|
@@ -300,9 +434,16 @@ export default async function Layout({
|
|
|
300
434
|
</div>
|
|
301
435
|
);
|
|
302
436
|
}
|
|
437
|
+
|
|
438
|
+
|
|
303
439
|
```
|
|
304
440
|
|
|
305
|
-
|
|
441
|
+
</details>
|
|
442
|
+
|
|
443
|
+
##### 任意状态结构(不推荐)
|
|
444
|
+
|
|
445
|
+
<details>
|
|
446
|
+
<Summary>示例</Summary>
|
|
306
447
|
|
|
307
448
|
```typescript
|
|
308
449
|
'use client'
|
|
@@ -311,12 +452,112 @@ import { useUrlState } from 'state-in-url/next';
|
|
|
311
452
|
const someObj = {};
|
|
312
453
|
|
|
313
454
|
function SettingsComponent() {
|
|
314
|
-
const {
|
|
455
|
+
const { urlState, setUrl, setState } = useUrlState<object>(someObj);
|
|
456
|
+
}
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
</details>
|
|
460
|
+
|
|
461
|
+
### React-Router 专用的 `useUrlState` 钩子
|
|
462
|
+
|
|
463
|
+
API 与 Next.js 版本相同,只是可以传递 [NavigateOptions](https://github.com/remix-run/react-router/blob/bc693ed9f39170bda13b9e1b282fb8e9d5925f66/packages/react-router/lib/context.ts#L99) 类型的选项。
|
|
464
|
+
|
|
465
|
+
[API 文档](packages/urlstate/react-router/useUrlState)
|
|
466
|
+
|
|
467
|
+
#### 示例
|
|
468
|
+
|
|
469
|
+
```typescript
|
|
470
|
+
export const form: Form = {
|
|
471
|
+
name: '',
|
|
472
|
+
age: undefined,
|
|
473
|
+
agree_to_terms: false,
|
|
474
|
+
tags: [],
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
type Form = {
|
|
478
|
+
name: string;
|
|
479
|
+
age?: number;
|
|
480
|
+
agree_to_terms: boolean;
|
|
481
|
+
tags: { id: string; value: { text: string; time: Date } }[];
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
```typescript
|
|
487
|
+
import { useUrlState } from 'state-in-url/react-router';
|
|
488
|
+
|
|
489
|
+
import { form } from './form';
|
|
490
|
+
|
|
491
|
+
function TagsComponent() {
|
|
492
|
+
const { urlState, setUrl, setState } = useUrlState(form);
|
|
493
|
+
|
|
494
|
+
const onChangeTags = React.useCallback(
|
|
495
|
+
(tag: (typeof tags)[number]) => {
|
|
496
|
+
setUrl((curr) => ({
|
|
497
|
+
...curr,
|
|
498
|
+
tags: curr.tags.find((t) => t.id === tag.id)
|
|
499
|
+
? curr.tags.filter((t) => t.id !== tag.id)
|
|
500
|
+
: curr.tags.concat(tag),
|
|
501
|
+
}));
|
|
502
|
+
},
|
|
503
|
+
[setUrl],
|
|
504
|
+
);
|
|
505
|
+
|
|
506
|
+
return (
|
|
507
|
+
<div>
|
|
508
|
+
<Field text="标签">
|
|
509
|
+
<div className="flex flex-wrap gap-2">
|
|
510
|
+
{tags.map((tag) => (
|
|
511
|
+
<Tag
|
|
512
|
+
active={!!urlState.tags.find((t) => t.id === tag.id)}
|
|
513
|
+
text={tag.value.text}
|
|
514
|
+
onClick={() => onChangeTags(tag)}
|
|
515
|
+
key={tag.id}
|
|
516
|
+
/>
|
|
517
|
+
))}
|
|
518
|
+
</div>
|
|
519
|
+
</Field>
|
|
520
|
+
|
|
521
|
+
<input value={urlState.name}
|
|
522
|
+
onChange={(ev) => { setState(curr => ({ ...curr, name: ev.target.value })) }}
|
|
523
|
+
// 可以立即更新状态,但根据需要同步到 URL
|
|
524
|
+
onBlur={() => setUrl()}
|
|
525
|
+
/>
|
|
526
|
+
</div>
|
|
527
|
+
);
|
|
315
528
|
}
|
|
529
|
+
|
|
530
|
+
const tags = [
|
|
531
|
+
{
|
|
532
|
+
id: '1',
|
|
533
|
+
value: { text: 'React.js', time: new Date('2024-07-17T04:53:17.000Z') },
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
id: '2',
|
|
537
|
+
value: { text: 'Next.js', time: new Date('2024-07-18T04:53:17.000Z') },
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
id: '3',
|
|
541
|
+
value: { text: 'TailwindCSS', time: new Date('2024-07-19T04:53:17.000Z') },
|
|
542
|
+
},
|
|
543
|
+
];
|
|
316
544
|
```
|
|
317
545
|
|
|
318
|
-
|
|
319
|
-
|
|
546
|
+
[示例代码](packages/example-react-router6/src/Form-for-test.tsx)
|
|
547
|
+
|
|
548
|
+
---
|
|
549
|
+
|
|
550
|
+
## 其他钩子和辅助工具
|
|
551
|
+
|
|
552
|
+
### 其他路由器的 `useUrlStateBase` 钩子
|
|
553
|
+
|
|
554
|
+
用于创建与其他路由器(如 react-router 或 tanstack router)兼容的 `useUrlState` 钩子。
|
|
555
|
+
|
|
556
|
+
[API 文档](packages/urlstate/useUrlStateBase)
|
|
557
|
+
|
|
558
|
+
### React.js 的 `useSharedState` 钩子
|
|
559
|
+
|
|
560
|
+
用于在任何 React 组件之间共享状态的钩子,已在 Next.js 和 Vite 中测试。
|
|
320
561
|
|
|
321
562
|
```typescript
|
|
322
563
|
'use client'
|
|
@@ -329,59 +570,96 @@ function SettingsComponent() {
|
|
|
329
570
|
}
|
|
330
571
|
```
|
|
331
572
|
|
|
332
|
-
[文档](packages/urlstate/useSharedState/README.md)
|
|
573
|
+
[API 文档](packages/urlstate/useSharedState/README.md)
|
|
333
574
|
|
|
334
|
-
|
|
575
|
+
### React.js 的 `useUrlEncode` 钩子
|
|
335
576
|
|
|
336
|
-
[文档](packages/urlstate/useUrlEncode/README.md)
|
|
577
|
+
[API 文档](packages/urlstate/useUrlEncode/README.md)
|
|
337
578
|
|
|
338
|
-
|
|
579
|
+
### `encodeState` 和 `decodeState` 辅助函数
|
|
339
580
|
|
|
340
|
-
[文档](packages/urlstate/encodeState/README.md)
|
|
581
|
+
[API 文档](packages/urlstate/encodeState/README.md)
|
|
341
582
|
|
|
342
|
-
|
|
583
|
+
### `encode` 和 `decode` 辅助函数
|
|
343
584
|
|
|
344
|
-
[文档](packages/urlstate/encoder/README.md)
|
|
585
|
+
[API 文档](packages/urlstate/encoder/README.md)
|
|
586
|
+
|
|
587
|
+
---
|
|
345
588
|
|
|
346
589
|
## 最佳实践
|
|
347
590
|
|
|
348
|
-
-
|
|
349
|
-
- 使用TypeScript
|
|
350
|
-
- 避免在URL
|
|
351
|
-
-
|
|
352
|
-
|
|
353
|
-
|
|
591
|
+
- 将状态结构定义为常量
|
|
592
|
+
- 使用 TypeScript 增强类型安全和自动补全
|
|
593
|
+
- 避免在 URL 参数中存储敏感信息(如 SSN、API 密钥等)
|
|
594
|
+
- 使用此 [扩展](https://marketplace.visualstudio.com/items?itemName=yoavbls.pretty-ts-errors) 以获得可读的 TS 错误提示
|
|
595
|
+
|
|
596
|
+
可以为状态片段创建状态钩子,并在应用程序中重复使用。例如:
|
|
597
|
+
```Typescript
|
|
598
|
+
type UserState = {
|
|
599
|
+
name: string;
|
|
600
|
+
age: number;
|
|
601
|
+
other: { id: string, value: number }[]
|
|
602
|
+
};
|
|
603
|
+
const userState = {
|
|
604
|
+
name: '',
|
|
605
|
+
age: 0,
|
|
606
|
+
other: [],
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
export const useUserState = () => {
|
|
610
|
+
const { urlState, setUrl, reset } = useUrlState(userState);
|
|
611
|
+
|
|
612
|
+
// 其他逻辑
|
|
613
|
+
|
|
614
|
+
// 导航到其他页面时重置查询参数
|
|
615
|
+
React.useEffect(() => {
|
|
616
|
+
return reset
|
|
617
|
+
}, [])
|
|
618
|
+
|
|
619
|
+
return { userState: urlState, setUserState: setUrl };;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
```
|
|
354
623
|
|
|
355
624
|
## 注意事项
|
|
356
625
|
|
|
357
|
-
1. 只能传递可序列化的值,`Function`、`BigInt
|
|
358
|
-
2. Vercel
|
|
359
|
-
3.
|
|
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 的计划。
|
|
360
629
|
|
|
361
|
-
|
|
630
|
+
---
|
|
362
631
|
|
|
363
|
-
|
|
632
|
+
## 其他
|
|
364
633
|
|
|
365
|
-
|
|
366
|
-
npm run dev
|
|
367
|
-
```
|
|
634
|
+
### 贡献指南与本地运行说明
|
|
368
635
|
|
|
369
|
-
|
|
636
|
+
参见 [贡献文档](CONTRIBUTING.md)
|
|
637
|
+
|
|
638
|
+
## 路线图
|
|
639
|
+
|
|
640
|
+
- [x] `Next.js` 钩子
|
|
641
|
+
- [x] `react-router` 钩子
|
|
642
|
+
- [ ] `remix` 钩子
|
|
643
|
+
- [ ] `svelte` 钩子
|
|
644
|
+
- [ ] `astro` 钩子
|
|
645
|
+
- [ ] 在 hash 中存储状态的钩子?
|
|
370
646
|
|
|
371
647
|
## 联系与支持
|
|
372
648
|
|
|
373
|
-
- 创建[GitHub issue](https://github.com/asmyshlyaev177/state-in-url/issues)
|
|
649
|
+
- 创建 [GitHub issue](https://github.com/asmyshlyaev177/state-in-url/issues) 以报告错误、请求功能或提出问题
|
|
374
650
|
|
|
375
|
-
## [更新日志](
|
|
651
|
+
## [更新日志](CHANGELOG.md)
|
|
376
652
|
|
|
377
653
|
## 许可证
|
|
378
654
|
|
|
379
|
-
本项目采用[MIT许可证](
|
|
655
|
+
本项目采用 [MIT 许可证](LICENSE)。
|
|
380
656
|
|
|
381
657
|
## 灵感来源
|
|
382
658
|
|
|
383
|
-
[
|
|
659
|
+
[NUQS](https://github.com/47ng/nuqs)
|
|
660
|
+
|
|
661
|
+
[在 Vue 中使用 URL 存储状态](https://dev.to/jacobandrewsky/using-url-to-store-state-in-vue-275c)
|
|
384
662
|
|
|
385
|
-
[在URL中存储状态](https://antonz.org/storing-state/)
|
|
663
|
+
[在 URL 中存储状态](https://antonz.org/storing-state/)
|
|
386
664
|
|
|
387
|
-
[NextJS useSearchParams](https://nextjs.org/docs/app/api-reference/functions/use-search-params)
|
|
665
|
+
[NextJS useSearchParams](https://nextjs.org/docs/app/api-reference/functions/use-search-params)
|
package/README.md
CHANGED
|
@@ -57,6 +57,8 @@ Share if it useful for you.
|
|
|
57
57
|
|
|
58
58
|
</div>
|
|
59
59
|
|
|
60
|
+
[Just Show Me Code!](#useurlstate)
|
|
61
|
+
|
|
60
62
|
## Why use `state-in-url`?
|
|
61
63
|
|
|
62
64
|
Store any user state in query parameters; imagine JSON in a browser URL. All of it with keeping types and structure of data, e.g. numbers will be decoded as numbers not strings, dates as dates, etc, objects and arrays supported.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{isSafari as e}from"../utils.mjs";const
|
|
1
|
+
import{isSafari as e}from"../utils.mjs";const n={date:"\u23F2",undefined:"\u2219undefined"};const d=e?330:70;export{n as SYMBOLS,d as TIMEOUT};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{decode as f,encode as m}from"../encoder/encoder.mjs";import{getParams as c}from"../utils.mjs";function a(r,e,n){const o=c(n);for(const[t,i]of Object.entries(r||{})){const s=e?.[t];if(JSON.stringify(i)!==JSON.stringify(s)){o.set(t,m(i))}}return o.toString()}function d(r,e){return{...e||{},...Object.fromEntries([...c(r).entries()].map(([n,o])=>{const t=e?.[n];return[n,f(o,t)??t]}))}}export{d as decodeState,a as encodeState};
|
package/dist/encoder/encoder.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{SYMBOLS as
|
|
1
|
+
import{SYMBOLS as n}from"../constants/constants.mjs";import{typeOf as u}from"../utils.mjs";function p(e){if(typeof e==="function"||typeof e==="symbol")return"";if(l(e)){return e}return JSON.stringify(structuredClone(e),o).replace(/'/g,"%27").replace(/"/g,"'")}function o(e,r){const i=u(r);if(i!=="object"&&i!=="array"){return f(r)}if(i==="object"){const t=r;for(const c of Object.keys(t)){t[c]=o(e,t[c])}return t}if(i==="array"){return r.map(t=>o(e,t))}return r}const f=e=>{switch(u(e)){case"date":return n.date+new Date(e).toISOString();case"undefined":return n.undefined;default:return e}};function y(e,r){return a(e.replace(/'/g,'"').replace(/%27/g,"'"),r)}function a(e,r){try{return JSON.parse(e,s)}catch{return r}}function s(e,r){return typeof r==="string"?d(r):r}const d=e=>{if(e===n.undefined)return void 0;if(e?.startsWith?.(n.date))return new Date(e.slice(1));return e};const g=new RegExp(`^(${n.undefined}|${n.date})`);const l=e=>g.test(String(e));export{y as decode,d as decodePrimitive,p as encode,f as encodePrimitive,a as parseJSON,s as reviver};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import
|
|
1
|
+
import a from"react";import{useRouter as C,useSearchParams as H}from"next/navigation";import{parseSPObj as d}from"../../parseSPObj.mjs";import{useUrlStateBase as g}from"../../useUrlStateBase/useUrlStateBase.mjs";import{routerHistory as U,filterUnknownParams as h,isSSR as v,filterUnknownParamsClient as w}from"../../utils.mjs";function E(t,r){const s="defaultState"in t?t.defaultState:t;const u="defaultState"in t?t.searchParams:r?.searchParams;const l="defaultState"in t?t.useHistory:r?.useHistory;const P="defaultState"in t?{scroll:t.scroll,replace:t.replace}:{scroll:r?.scroll,replace:r?.replace};const o=C();const b=a.useMemo(()=>({push:(...e)=>{if(l===void 0?true:!!l){U.push(...e)}else{o.push(...e)}},replace:(...e)=>{if(l===void 0?true:!!l){U.replace(...e)}else{o.replace(...e)}}}),[o]);const{state:n,updateState:c,updateUrl:i,reset:p,getState:k}=g(s,b,({parse:e})=>{return v?d(h(s,u),s):e(w(s,u))});const f=a.useMemo(()=>({...M,...P}),[]);const m=a.useCallback((e,j)=>i(e,{...f,...j}),[i]);const S=H();a.useLayoutEffect(()=>{c(h(s,d(Object.fromEntries([...S.entries()]),s)))},[S]);const y=a.useCallback(e=>{p({...f,...e})},[p]);return{setState:c,updateState:c,setUrl:m,updateUrl:m,urlState:n,state:n,reset:y,getState:k}}const M={replace:true,scroll:false};export{E as useUrlState};
|
package/dist/parseSPObj.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{decode as c}from"./encoder/encoder.mjs";function o(
|
|
1
|
+
import{decode as c}from"./encoder/encoder.mjs";function o(n,t){return{...t,...s(JSON.stringify(n),t)}}function s(n,t){try{return JSON.parse(n,i)}catch{return t}}const i=(n,t)=>{const r=typeof t==="string";const e=r&&c(t?.replace?.(/'/g,'"'));return n&&r?e:t};export{o as parseSPObj};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import
|
|
1
|
+
import a from"react";import{useNavigate as b,useSearchParams as g}from"react-router-dom";import{parseSPObj as k}from"../../parseSPObj.mjs";import{useUrlStateBase as C}from"../../useUrlStateBase/useUrlStateBase.mjs";import{routerHistory as P,filterUnknownParamsClient as y,assignValue as H,filterUnknownParams as h}from"../../utils.mjs";function j(t,l){const r="defaultState"in t?t.defaultState:t;const f="defaultState"in t?t.useHistory:l?.useHistory;const d="defaultState"in t?{replace:t.replace,preventScrollReset:t.preventScrollReset}:{replace:l?.replace,preventScrollReset:l?.preventScrollReset};const o=a.useMemo(()=>({...w,...d}),[]);const n=b();const U=a.useMemo(()=>f?P:{replace:(e,s)=>n(e,{...o,...s}),push:(e,s)=>n(e,{...o,...s})},[n]);const[c]=g();const{state:p,updateState:u,updateUrl:i,getState:v,reset:S}=C(r,U,({parse:e})=>e(y(r,c.entries())));const m=a.useCallback((e,s)=>i(e,{...o,...s}),[i]);a.useEffect(()=>{u(H(r,h(r,k(Object.fromEntries([...c.entries()]),r))))},[c]);const R=a.useCallback(e=>{S({...o,...e})},[S]);return{setState:u,updateState:u,setUrl:m,updateUrl:m,urlState:p,state:p,reset:R,getState:v}}const w={replace:true,preventScrollReset:true};export{j as useUrlState};
|
package/dist/subscribers.d.ts
CHANGED
|
@@ -6,6 +6,6 @@ export declare const subscribers: {
|
|
|
6
6
|
};
|
|
7
7
|
export declare const stateMap: {
|
|
8
8
|
get: <T extends JSONCompatible | undefined>(objKey: T) => T | undefined;
|
|
9
|
-
set: <
|
|
9
|
+
set: <T extends JSONCompatible>(objKey: T, value: T) => void;
|
|
10
10
|
};
|
|
11
11
|
export type Cb = () => void;
|
package/dist/subscribers.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const
|
|
1
|
+
const o=new WeakMap;const n=new WeakMap;const a={get(t){return n.get(t)||[]},add(t,e){const s=this.get(t);n.set(t,s.concat(e));return()=>this.remove(t,e)},remove(t,e){const s=this.get(t);const c=s.filter(r=>r!==e);if(c.length){n.set(t,c)}else{n.delete(t)}}};const i={get:function(t){return o.get(t)},set:function(t,e){o.set(t,e)}};export{i as stateMap,a as subscribers};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import
|
|
1
|
+
import c from"react";import{stateMap as n,subscribers as o}from"../subscribers.mjs";import{useInsertionEffect as l}from"../useInsertionEffect.mjs";import{isSSR as b,isEqual as d}from"../utils.mjs";function E(a,u){const t=c.useRef(a);const[f,i]=c.useState(()=>{if(b){return u?u?.():t.current}const e=n.get(t.current);if(!e){const r=u?.()||t.current;n.set(t.current,r);return r}else{return e}});const m=c.useCallback(e=>{const r=n.get(t.current);const S=typeof e==="function";const s=S?e(r):{...r,...e};if(d(r,s))return void 0;n.set(t.current,s);o.get(t.current).forEach(g=>g())},[]);l(()=>{const e=()=>{i(n.get(t.current)||t.current)};const r=o.add(t.current,e);return()=>{r()}},[]);const p=c.useCallback(()=>n.get(t.current)||t.current,[]);return{state:f,getState:p,setState:m}}export{E as useSharedState};
|
|
@@ -30,7 +30,7 @@ console.log(state); // Output: { name: 'Tom' }
|
|
|
30
30
|
*
|
|
31
31
|
* * Docs {@link https://github.com/asmyshlyaev177/state-in-url/tree/master/packages/urlstate/useUrlEncode#parse}
|
|
32
32
|
*/
|
|
33
|
-
parse: (strOrSearchParams: string | URLSearchParams) =>
|
|
33
|
+
parse: (strOrSearchParams: string | URLSearchParams) => typeof stateShape;
|
|
34
34
|
/**
|
|
35
35
|
Converts a state object to a URL query string.
|
|
36
36
|
* * Example:
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import
|
|
1
|
+
import r from"react";import{encodeState as a,decodeState as s}from"../encodeState/encodeState.mjs";import{typeOf as f}from"../utils.mjs";function i(e){const o=r.useCallback((t,n)=>{return f(t)==="object"?a(t,e,n):""},[e]);const c=r.useCallback(t=>s(t,e),[e]);return{parse:c,stringify:o}}export{i as useUrlEncode};
|
|
@@ -37,7 +37,7 @@ export declare function useUrlStateBase<T extends JSONCompatible>(defaultState:
|
|
|
37
37
|
parse: ReturnType<typeof useUrlEncode<T>>["parse"];
|
|
38
38
|
}) => T): {
|
|
39
39
|
updateState: (value: Partial<T> | ((currState: T) => T)) => void;
|
|
40
|
-
updateUrl: (value?: Partial<T> | ((currState: T) => T)
|
|
40
|
+
updateUrl: (value?: Parameters<(value: Partial<T> | ((currState: T) => T)) => void>[0], options?: Options) => void;
|
|
41
41
|
state: T;
|
|
42
42
|
reset: (options?: Options) => void;
|
|
43
43
|
getState: () => T;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import
|
|
1
|
+
import u from"react";import{TIMEOUT as y}from"../constants/constants.mjs";import{useInsertionEffect as L}from"../useInsertionEffect.mjs";import{useSharedState as T}from"../useSharedState/useSharedState.mjs";import{useUrlEncode as v}from"../useUrlEncode/useUrlEncode.mjs";import{filterUnknownParamsClient as R}from"../utils.mjs";function b(t,c,f){const{parse:s,stringify:r}=v(t);const{state:a,getState:n,setState:o}=T(t,()=>f?.({parse:s})||t);const l=u.useRef(void 0);L(()=>{const e=()=>{const m=s(R(t,P()));o(m)};window.addEventListener($,e);return()=>{window.removeEventListener($,e);clearTimeout(l.current)}},[o]);const i=u.useRef([]);const w=u.useCallback((e,m)=>{const d=typeof e==="function"?e(n()):{...n(),...e};const h=r(d,C(t));const S=`${window.location.pathname}${h.length?"?":""}${h}${window.location.hash}`;const g=`${window.location.pathname}${window.location.search}${window.location.hash}`;if(S===g)return;o(d);const{replace:k,...E}=m||{};i.current.push([k?"replace":"push",S,E]);clearTimeout(l.current);queueMicrotask(()=>{if(!i.current.length)return;l.current=setTimeout(()=>{const p=i.current.at(-1);i.current=[];c[p[0]](p[1],p[2])},y)})},[c,r,n]);const U=u.useCallback(e=>{o(t);w(t,e)},[w,o]);return{updateState:o,updateUrl:w,state:a,reset:U,getState:n}}function C(t){const c=Object.keys(t);const f=window.location.search;const s=new URLSearchParams(f);const r=new URLSearchParams;for(const[a,n]of s){!c.includes(a)&&r.set(a,n)}return r}const $="popstate";function P(){return typeof window!=="undefined"&&window.location.search||""}export{b as useUrlStateBase};
|
package/dist/utils.d.ts
CHANGED
|
@@ -22,7 +22,7 @@ export type UnknownObj = object | {
|
|
|
22
22
|
[key: string]: unknown;
|
|
23
23
|
};
|
|
24
24
|
export declare const isEqual: (val1: unknown, val2: unknown) => boolean;
|
|
25
|
-
export declare function filterUnknownParamsClient<T extends object>(shape: T): string;
|
|
25
|
+
export declare function filterUnknownParamsClient<T extends object>(shape: T, params: string | URLSearchParams | Record<string, string> | object): string;
|
|
26
26
|
export declare function filterUnknownParams<T extends object>(shape: T, searchParams?: object): T;
|
|
27
27
|
export declare function filterUnknown<T extends object>(shape: T, entries: [key: string, value: string][]): string[][];
|
|
28
28
|
export declare function assignValue<T extends object>(shape: T, newVal: Partial<T>): T;
|
package/dist/utils.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const
|
|
1
|
+
const a=n=>{const t=typeof n;const s=n===null;const r=Array.isArray(n);const o=n instanceof Date;const e=!s&&!o&&!r&&t==="object";return s&&"null"||o&&"date"||r&&"array"||e&&"object"||t};const c=typeof window==="undefined";const f=n=>new URLSearchParams(typeof n==="string"?l(n):n?.toString?.()||"");const l=n=>n.split("?")?.[1]||n||"";const u=(n,t)=>JSON.stringify(n)===JSON.stringify(t);function w(n,t){const s=new URLSearchParams;const r=i(n,[...new URLSearchParams(t||"").entries()]);for(const[o,e]of r){s.set(o,e)}return s.toString()}function p(n,t){return Object.fromEntries(i(n,Object.entries(t||{})))}function i(n,t){const s=Object.keys(n);const r=[];for(let o=0;o<t.length;o++){if(s.includes(t[o]?.[0])){r.push([t[o][0].replace(/\+/g," "),t[o][1]])}}return r}function y(n,t){const s=Object.assign({},n);for(const[r]of Object.entries(n)){const o=r;const e=t[o]!==void 0;s[o]=e?t[o]:n[o]}return s}const g={push:n=>{window&&window.history.pushState(null,"",n)},replace:n=>{window&&window.history.replaceState(null,"",n)}};const S=/apple/i.test(!c&&navigator?.userAgent||"");export{y as assignValue,i as filterUnknown,p as filterUnknownParams,w as filterUnknownParamsClient,f as getParams,u as isEqual,c as isSSR,S as isSafari,g as routerHistory,a as typeOf};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "state-in-url",
|
|
3
|
-
"version": "4.2.
|
|
3
|
+
"version": "4.2.1",
|
|
4
4
|
"description": "Store state in URL as in object, types and structure are preserved, with TS validation. Same API as React.useState, wthout any hasssle or boilerplate. Next.js@14-15 and react-router@6-7.",
|
|
5
5
|
"homepage": "https://state-in-url.dev",
|
|
6
6
|
"repository": {
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
},
|
|
15
15
|
"publishConfig": {
|
|
16
16
|
"registry": "https://registry.npmjs.org/",
|
|
17
|
-
"tag": "latest"
|
|
17
|
+
"tag": "latest",
|
|
18
|
+
"provenance": false
|
|
18
19
|
},
|
|
19
20
|
"license": "MIT",
|
|
20
21
|
"funding": {
|
|
@@ -352,8 +353,9 @@
|
|
|
352
353
|
"@vitest/coverage-istanbul": "^2.1.5",
|
|
353
354
|
"auto-changelog": "^2.4.0",
|
|
354
355
|
"babel-plugin-module-resolver": "^5.0.2",
|
|
355
|
-
"chokidar": "^4.0.
|
|
356
|
+
"chokidar": "^4.0.3",
|
|
356
357
|
"commitizen": "^4.3.0",
|
|
358
|
+
"commitlint": "^19.8.0",
|
|
357
359
|
"conventional-recommended-bump": "^10.0.0",
|
|
358
360
|
"cross-env": "^7.0.3",
|
|
359
361
|
"cz": "^1.8.2",
|
|
@@ -374,18 +376,18 @@
|
|
|
374
376
|
"prettier": "^3.2.5",
|
|
375
377
|
"prettier-plugin-tailwindcss": "^0.6.8",
|
|
376
378
|
"release-it": "^17.6.0",
|
|
377
|
-
"rollup": "^4.
|
|
378
|
-
"rollup-plugin-esbuild": "^6.
|
|
379
|
+
"rollup": "^4.39.0",
|
|
380
|
+
"rollup-plugin-esbuild": "^6.2.1",
|
|
379
381
|
"rollup-plugin-filesize": "^10.0.0",
|
|
380
382
|
"rollup-plugin-sourcemaps": "^0.6.3",
|
|
381
383
|
"semantic-release": "^24.1.0",
|
|
382
384
|
"standard-version": "^9.5.0",
|
|
383
385
|
"ts-node": "^10.9.2",
|
|
384
|
-
"typescript": "^5.
|
|
386
|
+
"typescript": "^5.8.2",
|
|
385
387
|
"unplugin-auto-import": "^0.18.5",
|
|
386
388
|
"vitest": "^2.1.5",
|
|
387
389
|
"wait-on": "^8.0.1",
|
|
388
|
-
"wireit": "^0.14.
|
|
390
|
+
"wireit": "^0.14.11"
|
|
389
391
|
},
|
|
390
392
|
"keywords": [
|
|
391
393
|
"state in url",
|