state-in-url 1.0.0 → 1.0.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 +305 -0
- package/README.KO.md +305 -0
- package/README.md +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/urlstate/useUrlState/useUrlState.d.ts +1 -1
- package/package.json +2 -2
package/README.CN.md
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
[English](./README.md) | [한국어](./README.KO.md) | 简体中文
|
|
2
|
+
|
|
3
|
+
<h1 align="center">state-in-url</h1>
|
|
4
|
+
|
|
5
|
+
<div align="center">
|
|
6
|
+
<img src="/assets/logo.svg?raw=true" alt="库标志"/>
|
|
7
|
+
|
|
8
|
+
<div>轻松在不相关的 React 组件之间共享状态,具有 IDE 自动完成和 TS 验证功能。无需任何麻烦或样板代码。</div>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<div align="center">
|
|
12
|
+
<hr />
|
|
13
|
+
|
|
14
|
+
[](https://www.npmjs.com/package/state-in-url)
|
|
15
|
+

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

|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<div align="center">
|
|
22
|
+
|
|
23
|
+
<h4 align="center">如果发现bug,请不要犹豫,立即提出问题</h4>
|
|
24
|
+
|
|
25
|
+

|
|
26
|
+
|
|
27
|
+
<a href="https://state-in-url-asmyshlyaev177.vercel.app/" target="_blank">DEMO</a> |
|
|
28
|
+
<a href ="https://codesandbox.io/p/github/asmyshlyaev177/state-in-url/main?layout=%257B%2522sidebarPanel%2522%253A%2522EXPLORER%2522%252C%2522rootPanelGroup%2522%253A%257B%2522direction%2522%253A%2522horizontal%2522%252C%2522contentType%2522%253A%2522UNKNOWN%2522%252C%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522id%2522%253A%2522ROOT_LAYOUT%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522UNKNOWN%2522%252C%2522direction%2522%253A%2522vertical%2522%252C%2522id%2522%253A%2522clyk5bd9y00062v6jspcfrkx7%2522%252C%2522sizes%2522%253A%255B100%252C0%255D%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522EDITOR%2522%252C%2522direction%2522%253A%2522horizontal%2522%252C%2522id%2522%253A%2522EDITOR%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL%2522%252C%2522contentType%2522%253A%2522EDITOR%2522%252C%2522id%2522%253A%2522clyk5bd9x00022v6jyg71cr9e%2522%257D%255D%257D%252C%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522SHELLS%2522%252C%2522direction%2522%253A%2522horizontal%2522%252C%2522id%2522%253A%2522SHELLS%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL%2522%252C%2522contentType%2522%253A%2522SHELLS%2522%252C%2522id%2522%253A%2522clyk5bd9x00042v6jsos2y043%2522%257D%255D%257D%255D%257D%252C%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522DEVTOOLS%2522%252C%2522direction%2522%253A%2522vertical%2522%252C%2522id%2522%253A%2522DEVTOOLS%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL%2522%252C%2522contentType%2522%253A%2522DEVTOOLS%2522%252C%2522id%2522%253A%2522clyk5bd9x00052v6j5r632b12%2522%257D%255D%257D%255D%252C%2522sizes%2522%253A%255B50%252C50%255D%257D%252C%2522tabbedPanels%2522%253A%257B%2522clyk5bd9x00022v6jyg71cr9e%2522%253A%257B%2522tabs%2522%253A%255B%257B%2522id%2522%253A%2522clyk5bd9w00012v6j83rq3bvo%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522type%2522%253A%2522FILE%2522%252C%2522filepath%2522%253A%2522%252FREADME.md%2522%252C%2522state%2522%253A%2522IDLE%2522%257D%255D%252C%2522id%2522%253A%2522clyk5bd9x00022v6jyg71cr9e%2522%252C%2522activeTabId%2522%253A%2522clyk5bd9w00012v6j83rq3bvo%2522%257D%252C%2522clyk5bd9x00052v6j5r632b12%2522%253A%257B%2522id%2522%253A%2522clyk5bd9x00052v6j5r632b12%2522%252C%2522activeTabId%2522%253A%2522clyk5bdjx000b2v6jfc8ae464%2522%252C%2522tabs%2522%253A%255B%257B%2522type%2522%253A%2522SETUP_TASKS%2522%252C%2522id%2522%253A%2522clyk5bdjx000b2v6jfc8ae464%2522%252C%2522mode%2522%253A%2522permanent%2522%257D%252C%257B%2522type%2522%253A%2522UNASSIGNED_PORT%2522%252C%2522port%2522%253A2222%252C%2522id%2522%253A%2522clyk5cjbo004d2v6j3u55k74g%2522%252C%2522mode%2522%253A%2522permanent%2522%257D%252C%257B%2522type%2522%253A%2522ENV_SETUP%2522%252C%2522id%2522%253A%2522clyk5h8dp000r2v6j0r7kc7qq%2522%252C%2522mode%2522%253A%2522permanent%2522%257D%255D%257D%252C%2522clyk5bd9x00042v6jsos2y043%2522%253A%257B%2522id%2522%253A%2522clyk5bd9x00042v6jsos2y043%2522%252C%2522activeTabId%2522%253A%2522clyk5cbuv001q2v6joegrxxv6%2522%252C%2522tabs%2522%253A%255B%257B%2522id%2522%253A%2522clyk5bd9x00032v6jtfhj316o%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522type%2522%253A%2522NEW_TERMINAL%2522%257D%252C%257B%2522type%2522%253A%2522TASK_LOG%2522%252C%2522taskId%2522%253A%2522dev%2522%252C%2522id%2522%253A%2522clyk5cbuv001q2v6joegrxxv6%2522%252C%2522mode%2522%253A%2522permanent%2522%257D%252C%257B%2522type%2522%253A%2522TASK_LOG%2522%252C%2522taskId%2522%253A%2522cleanupDist%2522%252C%2522id%2522%253A%2522clyk5dgvo005i2v6jj950sxft%2522%252C%2522mode%2522%253A%2522permanent%2522%257D%255D%257D%257D%252C%2522showDevtools%2522%253Atrue%252C%2522showShells%2522%253Atrue%252C%2522showSidebar%2522%253Atrue%252C%2522sidebarPanelSize%2522%253A15%257D" target="_blank">Demo on Codesandbox</a>
|
|
29
|
+
|
|
30
|
+
<hr />
|
|
31
|
+
|
|
32
|
+
给项目加个 ⭐️ 以示支持!
|
|
33
|
+
|
|
34
|
+
<hr />
|
|
35
|
+
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
## 为什么使用 state-in-url?
|
|
39
|
+
|
|
40
|
+
`state-in-url` 是一个简单的状态管理工具,具有 URL 同步功能。
|
|
41
|
+
|
|
42
|
+
# 使用场景
|
|
43
|
+
|
|
44
|
+
- 🙃 在不改变 URL 的情况下在不同组件间共享状态,是信号和其他状态管理工具的良好替代品
|
|
45
|
+
- 🔗 包含完整应用程序状态的可共享 URL
|
|
46
|
+
- 🔄 在页面重新加载时轻松保持状态
|
|
47
|
+
- 🧠 在不相关的客户端组件间同步数据
|
|
48
|
+
- 🧮 在 URL 中存储未保存的用户表单
|
|
49
|
+
|
|
50
|
+
# 特性
|
|
51
|
+
|
|
52
|
+
- 🧩 **简单**: 无需提供者、减速器、样板代码或新概念
|
|
53
|
+
- 📘 **TypeScript 支持和类型安全**: 保留数据类型和结构,通过 IDE 建议、强类型和 JSDoc 注释增强开发体验
|
|
54
|
+
- ⚛️ **框架灵活性**: 为 Next.js 和 React.js 应用程序提供单独的钩子,以及纯 JS 函数
|
|
55
|
+
- ⚙ **测试充分**: 单元测试和 Playwright 测试
|
|
56
|
+
- ⚡ **快速**: 最小化重新渲染
|
|
57
|
+
- 🪶 **轻量级**: 零依赖,占用空间小
|
|
58
|
+
|
|
59
|
+
## 目录
|
|
60
|
+
|
|
61
|
+
- [安装](#安装)
|
|
62
|
+
- [Next.js 的 `useUrlState`](#nextjs-的-useurlstate-钩子)
|
|
63
|
+
- [React.js 的 `useUrlEncode`](#reactjs-的-useurlencode-钩子)
|
|
64
|
+
- [纯 JS 使用的 `encodeState` 和 `decodeState`](#纯-js-使用的-encodestate-和-decodestate-辅助函数)
|
|
65
|
+
- [自动同步状态到 URL](#自动同步状态)
|
|
66
|
+
- [低级 `encode` 和 `decode` 函数](#encode-和-decode-辅助函数)
|
|
67
|
+
- [最佳实践](#最佳实践)
|
|
68
|
+
- [注意事项](#注意事项)
|
|
69
|
+
- [联系与支持](#联系与支持)
|
|
70
|
+
- [更新日志](#更新日志)
|
|
71
|
+
- [许可证](#许可证)
|
|
72
|
+
- [灵感来源](#灵感来源)
|
|
73
|
+
|
|
74
|
+
## 安装
|
|
75
|
+
|
|
76
|
+
### 1. 安装包
|
|
77
|
+
|
|
78
|
+
```sh
|
|
79
|
+
# npm
|
|
80
|
+
npm install --save state-in-url
|
|
81
|
+
# yarn
|
|
82
|
+
yarn add state-in-url
|
|
83
|
+
# pnpm
|
|
84
|
+
pnpm add state-in-url
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### 2. 编辑 tsconfig.json
|
|
88
|
+
|
|
89
|
+
在 `tsconfig.json` 中设置 `"moduleResolution": "Node16"` 或 `"moduleResolution": "NodeNext"`
|
|
90
|
+
|
|
91
|
+
## Next.js 的 useUrlState 钩子
|
|
92
|
+
|
|
93
|
+
[文档](packages/urlstate/useUrlState/README.md)
|
|
94
|
+
|
|
95
|
+
`useUrlState` 是一个为 Next.js 应用程序设计的自定义 React 钩子,使客户端组件之间的通信变得简单。它允许你共享任何复杂的状态并将其与 URL 搜索参数同步,提供了一种在页面重新加载时保持状态和通过 URL 共享应用程序状态的方法。
|
|
96
|
+
|
|
97
|
+
### 使用示例
|
|
98
|
+
|
|
99
|
+
#### 基本用法
|
|
100
|
+
|
|
101
|
+
1. 定义状态形状
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
// countState.ts
|
|
105
|
+
// 状态形状应该存储在一个常量中,不要直接传递对象
|
|
106
|
+
export const countState: CountState = { count: 0 }
|
|
107
|
+
|
|
108
|
+
type CountState = { count: number }
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
2. 导入并使用
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
'use client'
|
|
115
|
+
import { useUrlState } from 'state-in-url';
|
|
116
|
+
|
|
117
|
+
import { countState } from './countState';
|
|
118
|
+
|
|
119
|
+
function MyComponent() {
|
|
120
|
+
// 用于从服务器组件使用 searchParams
|
|
121
|
+
// 例如 export default async function Home({ searchParams }: { searchParams: object }) {
|
|
122
|
+
// const { state, updateState, updateUrl } = useUrlState(countState, searchParams);
|
|
123
|
+
const { state, updateState, updateUrl } = useUrlState(countState);
|
|
124
|
+
|
|
125
|
+
// 不会让你意外地直接修改状态,需要 TS
|
|
126
|
+
// state.count = 2 // <- 错误
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<div>
|
|
130
|
+
<p>计数: {state.count}</p>
|
|
131
|
+
|
|
132
|
+
<button onClick={() => updateUrl({ count: state.count + 1 }), { replace: true }}>
|
|
133
|
+
增加 (更新 URL)
|
|
134
|
+
</button>
|
|
135
|
+
|
|
136
|
+
// 与 React.useState 相同的 API
|
|
137
|
+
<button onClick={() => updateState(currState => ({...currState, count: currState.count + 1 }) )}>
|
|
138
|
+
增加 (仅本地)
|
|
139
|
+
</button>
|
|
140
|
+
<button onClick={() => updateUrl()}>
|
|
141
|
+
同步更改到 URL
|
|
142
|
+
// 或者不同步,只共享状态
|
|
143
|
+
</button>
|
|
144
|
+
|
|
145
|
+
<button onClick={() => updateUrl(state)}>
|
|
146
|
+
重置
|
|
147
|
+
</button>
|
|
148
|
+
</div>
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
#### 复杂状态形状示例
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
interface UserSettings {
|
|
157
|
+
theme: 'light' | 'dark';
|
|
158
|
+
fontSize: number;
|
|
159
|
+
notifications: boolean;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export const userSettings: UserSettings {
|
|
163
|
+
theme: 'light',
|
|
164
|
+
fontSize: 16,
|
|
165
|
+
notifications: true,
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
'use client'
|
|
171
|
+
import { useUrlState } from 'state-in-url';
|
|
172
|
+
|
|
173
|
+
import { userSettings } from './userSettings';
|
|
174
|
+
|
|
175
|
+
function SettingsComponent() {
|
|
176
|
+
// `state` 将从 UserSettings 类型推断!
|
|
177
|
+
const { state, updateUrl } = useUrlState(userSettings);
|
|
178
|
+
|
|
179
|
+
const toggleTheme = () => {
|
|
180
|
+
updateUrl(current => ({
|
|
181
|
+
...current,
|
|
182
|
+
theme: current.theme === 'light' ? 'dark' : 'light',
|
|
183
|
+
}));
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// 空闲时同步状态到 url
|
|
187
|
+
const timer = React.useRef(0 as unknown as NodeJS.Timeout);
|
|
188
|
+
React.useEffect(() => {
|
|
189
|
+
clearTimeout(timer.current);
|
|
190
|
+
timer.current = setTimeout(() => {
|
|
191
|
+
// 将按内容而不是引用比较状态,并仅对新值触发更新
|
|
192
|
+
updateUrl(state);
|
|
193
|
+
}, 500);
|
|
194
|
+
|
|
195
|
+
return () => {
|
|
196
|
+
clearTimeout(timer.current);
|
|
197
|
+
};
|
|
198
|
+
}, [state, updateUrl]);
|
|
199
|
+
|
|
200
|
+
return (
|
|
201
|
+
<div>
|
|
202
|
+
<h2>用户设置</h2>
|
|
203
|
+
<p>主题: {state.theme}</p>
|
|
204
|
+
<p>字体大小: {state.fontSize}px</p>
|
|
205
|
+
<button onClick={toggleTheme}>切换主题</button>
|
|
206
|
+
{/* 其他用于更新其他设置的 UI 元素 */}
|
|
207
|
+
</div>
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
...
|
|
211
|
+
|
|
212
|
+
// 其他组件
|
|
213
|
+
function Component() {
|
|
214
|
+
const { state } = useUrlState(defaultSettings);
|
|
215
|
+
|
|
216
|
+
return (
|
|
217
|
+
<div>
|
|
218
|
+
<p>通知功能 {state.notifications ? '开启' : '关闭'}</p>
|
|
219
|
+
</div>
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
#### 自动同步状态
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
const timer = React.useRef(0 as unknown as NodeJS.Timeout);
|
|
228
|
+
React.useEffect(() => {
|
|
229
|
+
clearTimeout(timer.current);
|
|
230
|
+
timer.current = setTimeout(() => {
|
|
231
|
+
// 将按内容而不是引用比较状态,并仅对新值触发更新
|
|
232
|
+
updateUrl(state);
|
|
233
|
+
}, 500);
|
|
234
|
+
|
|
235
|
+
return () => {
|
|
236
|
+
clearTimeout(timer.current);
|
|
237
|
+
};
|
|
238
|
+
}, [state, updateUrl]);
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
#### 任意状态形状 (不推荐)
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
'use client'
|
|
245
|
+
import { useUrlState } from 'state-in-url';
|
|
246
|
+
|
|
247
|
+
const someObj = {};
|
|
248
|
+
|
|
249
|
+
function SettingsComponent() {
|
|
250
|
+
const { state, updateUrl, updateState } = useUrlState<object>(someObj);
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## React.js 的 `useUrlEncode` 钩子
|
|
255
|
+
|
|
256
|
+
[文档](packages/urlstate/useUrlEncode/README.md)
|
|
257
|
+
|
|
258
|
+
## 纯 JS 使用的 `encodeState` 和 `decodeState` 辅助函数
|
|
259
|
+
|
|
260
|
+
[文档](packages/urlstate/encodeState/README.md)
|
|
261
|
+
|
|
262
|
+
## `encode` 和 `decode` 辅助函数
|
|
263
|
+
|
|
264
|
+
[文档](packages/urlstate/encoder/README.md)
|
|
265
|
+
|
|
266
|
+
## 最佳实践
|
|
267
|
+
|
|
268
|
+
- 将状态形状定义为常量以确保一致性
|
|
269
|
+
- 使用 TypeScript 以增强类型安全性和自动完成功能
|
|
270
|
+
- 避免在 URL 参数中存储敏感信息
|
|
271
|
+
- 对频繁更新使用 `updateState`,使用 `updateUrl` 同步更改到 url
|
|
272
|
+
- 使用此[扩展](https://marketplace.visualstudio.com/items?itemName=yoavbls.pretty-ts-errors)以获得可读的 TS 错误
|
|
273
|
+
|
|
274
|
+
## 注意事项
|
|
275
|
+
|
|
276
|
+
1. 只能传递可序列化的值,`Function`、`BigInt` 或 `Symbol` 不起作用,`ArrayBuffer` 之类的东西也可能不行。
|
|
277
|
+
2. Vercel 服务器将头部大小(查询字符串和其他内容)限制为 **14KB**,因此请将 URL 状态保持在约 5000 字以内。 https://vercel.com/docs/errors/URL_TOO_LONG
|
|
278
|
+
|
|
279
|
+
## 本地运行
|
|
280
|
+
|
|
281
|
+
克隆此仓库,运行 `npm install` 然后
|
|
282
|
+
|
|
283
|
+
```sh
|
|
284
|
+
npm run dev
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
转到 [localhost:3000](http://localhost:3000)
|
|
288
|
+
|
|
289
|
+
## 联系与支持
|
|
290
|
+
|
|
291
|
+
- 创建 [GitHub issue](https://github.com/asmyshlyaev177/state-in-url/issues) 以报告错误、请求功能或提出问题
|
|
292
|
+
|
|
293
|
+
## [更新日志](https://github.com/asmyshlyaev177/state-in-url/blob/main/CHANGELOG.md)
|
|
294
|
+
|
|
295
|
+
## 许可证
|
|
296
|
+
|
|
297
|
+
本项目采用 [MIT 许可证](https://github.com/asmyshlyaev177/state-in-url/blob/main/LICENSE)。
|
|
298
|
+
|
|
299
|
+
## 灵感来源
|
|
300
|
+
|
|
301
|
+
[在 Vue 中使用 URL 存储状态](https://dev.to/jacobandrewsky/using-url-to-store-state-in-vue-275c)
|
|
302
|
+
|
|
303
|
+
[在 URL 中存储状态](https://antonz.org/storing-state/)
|
|
304
|
+
|
|
305
|
+
[NextJS useSearchParams](https://nextjs.org/docs/app/api-reference/functions/use-search-params)
|
package/README.KO.md
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
[English](./README.md) | 한국어 | [简体中文](./README.CN.md)
|
|
2
|
+
|
|
3
|
+
<h1 align="center">state-in-url</h1>
|
|
4
|
+
|
|
5
|
+
<div align="center">
|
|
6
|
+
<img src="/assets/logo.svg?raw=true" alt="라이브러리 로고"/>
|
|
7
|
+
|
|
8
|
+
<div>IDE 자동 완성 및 TS 검증 기능을 갖춘 관련 없는 React 컴포넌트 간 상태 공유를 쉽게 할 수 있습니다. 번거로움이나 보일러플레이트 없이 말이죠.</div>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<div align="center">
|
|
12
|
+
<hr />
|
|
13
|
+
|
|
14
|
+
[](https://www.npmjs.com/package/state-in-url)
|
|
15
|
+

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

|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<div align="center">
|
|
22
|
+
|
|
23
|
+
<h4 align="center">버그를 발견하셨다면 주저하지 마시고 이슈를 열어주세요</h4>
|
|
24
|
+
|
|
25
|
+

|
|
26
|
+
|
|
27
|
+
<a href="https://state-in-url-asmyshlyaev177.vercel.app/" target="_blank">DEMO</a> |
|
|
28
|
+
<a href ="https://codesandbox.io/p/github/asmyshlyaev177/state-in-url/main?layout=%257B%2522sidebarPanel%2522%253A%2522EXPLORER%2522%252C%2522rootPanelGroup%2522%253A%257B%2522direction%2522%253A%2522horizontal%2522%252C%2522contentType%2522%253A%2522UNKNOWN%2522%252C%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522id%2522%253A%2522ROOT_LAYOUT%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522UNKNOWN%2522%252C%2522direction%2522%253A%2522vertical%2522%252C%2522id%2522%253A%2522clyk5bd9y00062v6jspcfrkx7%2522%252C%2522sizes%2522%253A%255B100%252C0%255D%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522EDITOR%2522%252C%2522direction%2522%253A%2522horizontal%2522%252C%2522id%2522%253A%2522EDITOR%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL%2522%252C%2522contentType%2522%253A%2522EDITOR%2522%252C%2522id%2522%253A%2522clyk5bd9x00022v6jyg71cr9e%2522%257D%255D%257D%252C%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522SHELLS%2522%252C%2522direction%2522%253A%2522horizontal%2522%252C%2522id%2522%253A%2522SHELLS%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL%2522%252C%2522contentType%2522%253A%2522SHELLS%2522%252C%2522id%2522%253A%2522clyk5bd9x00042v6jsos2y043%2522%257D%255D%257D%255D%257D%252C%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522DEVTOOLS%2522%252C%2522direction%2522%253A%2522vertical%2522%252C%2522id%2522%253A%2522DEVTOOLS%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL%2522%252C%2522contentType%2522%253A%2522DEVTOOLS%2522%252C%2522id%2522%253A%2522clyk5bd9x00052v6j5r632b12%2522%257D%255D%257D%255D%252C%2522sizes%2522%253A%255B50%252C50%255D%257D%252C%2522tabbedPanels%2522%253A%257B%2522clyk5bd9x00022v6jyg71cr9e%2522%253A%257B%2522tabs%2522%253A%255B%257B%2522id%2522%253A%2522clyk5bd9w00012v6j83rq3bvo%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522type%2522%253A%2522FILE%2522%252C%2522filepath%2522%253A%2522%252FREADME.md%2522%252C%2522state%2522%253A%2522IDLE%2522%257D%255D%252C%2522id%2522%253A%2522clyk5bd9x00022v6jyg71cr9e%2522%252C%2522activeTabId%2522%253A%2522clyk5bd9w00012v6j83rq3bvo%2522%257D%252C%2522clyk5bd9x00052v6j5r632b12%2522%253A%257B%2522id%2522%253A%2522clyk5bd9x00052v6j5r632b12%2522%252C%2522activeTabId%2522%253A%2522clyk5bdjx000b2v6jfc8ae464%2522%252C%2522tabs%2522%253A%255B%257B%2522type%2522%253A%2522SETUP_TASKS%2522%252C%2522id%2522%253A%2522clyk5bdjx000b2v6jfc8ae464%2522%252C%2522mode%2522%253A%2522permanent%2522%257D%252C%257B%2522type%2522%253A%2522UNASSIGNED_PORT%2522%252C%2522port%2522%253A2222%252C%2522id%2522%253A%2522clyk5cjbo004d2v6j3u55k74g%2522%252C%2522mode%2522%253A%2522permanent%2522%257D%252C%257B%2522type%2522%253A%2522ENV_SETUP%2522%252C%2522id%2522%253A%2522clyk5h8dp000r2v6j0r7kc7qq%2522%252C%2522mode%2522%253A%2522permanent%2522%257D%255D%257D%252C%2522clyk5bd9x00042v6jsos2y043%2522%253A%257B%2522id%2522%253A%2522clyk5bd9x00042v6jsos2y043%2522%252C%2522activeTabId%2522%253A%2522clyk5cbuv001q2v6joegrxxv6%2522%252C%2522tabs%2522%253A%255B%257B%2522id%2522%253A%2522clyk5bd9x00032v6jtfhj316o%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522type%2522%253A%2522NEW_TERMINAL%2522%257D%252C%257B%2522type%2522%253A%2522TASK_LOG%2522%252C%2522taskId%2522%253A%2522dev%2522%252C%2522id%2522%253A%2522clyk5cbuv001q2v6joegrxxv6%2522%252C%2522mode%2522%253A%2522permanent%2522%257D%252C%257B%2522type%2522%253A%2522TASK_LOG%2522%252C%2522taskId%2522%253A%2522cleanupDist%2522%252C%2522id%2522%253A%2522clyk5dgvo005i2v6jj950sxft%2522%252C%2522mode%2522%253A%2522permanent%2522%257D%255D%257D%257D%252C%2522showDevtools%2522%253Atrue%252C%2522showShells%2522%253Atrue%252C%2522showSidebar%2522%253Atrue%252C%2522sidebarPanelSize%2522%253A15%257D" target="_blank">Demo on Codesandbox</a>
|
|
29
|
+
|
|
30
|
+
<hr />
|
|
31
|
+
|
|
32
|
+
프로젝트에 ⭐️을 추가하여 지원해주세요!
|
|
33
|
+
|
|
34
|
+
<hr />
|
|
35
|
+
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
## state-in-url을 사용하는 이유?
|
|
39
|
+
|
|
40
|
+
`state-in-url`은 URL 동기화 기능을 갖춘 간단한 상태 관리 도구입니다.
|
|
41
|
+
|
|
42
|
+
# 사용 사례
|
|
43
|
+
|
|
44
|
+
- 🙃 URL을 변경하지 않고 다른 컴포넌트 간 상태 공유, 시그널 및 기타 상태 관리 도구의 좋은 대안
|
|
45
|
+
- 🔗 전체 애플리케이션 상태를 포함한 공유 가능한 URL
|
|
46
|
+
- 🔄 페이지 새로고침 시 쉬운 상태 유지
|
|
47
|
+
- 🧠 관련 없는 클라이언트 컴포넌트 간 데이터 동기화
|
|
48
|
+
- 🧮 URL에 저장되지 않은 사용자 양식 저장
|
|
49
|
+
|
|
50
|
+
# 특징
|
|
51
|
+
|
|
52
|
+
- 🧩 **간단함**: 프로바이더, 리듀서, 보일러플레이트 또는 새로운 개념 없음
|
|
53
|
+
- 📘 **TypeScript 지원 및 타입 안전성**: 데이터 타입과 구조 보존, IDE 제안, 강력한 타이핑 및 JSDoc 주석을 통한 개발자 경험 향상
|
|
54
|
+
- ⚛️ **프레임워크 유연성**: Next.js 및 React.js 애플리케이션을 위한 별도의 훅, 그리고 순수 JS를 위한 함수
|
|
55
|
+
- ⚙ **잘 테스트됨**: 단위 테스트 및 Playwright 테스트
|
|
56
|
+
- ⚡ **빠름**: 최소한의 리렌더링
|
|
57
|
+
- 🪶 **경량**: 더 작은 공간을 위한 제로 의존성
|
|
58
|
+
|
|
59
|
+
## 목차
|
|
60
|
+
|
|
61
|
+
- [설치](#설치)
|
|
62
|
+
- [Next.js용 `useUrlState`](#nextjs용-useurlstate-훅)
|
|
63
|
+
- [React.js용 `useUrlEncode`](#reactjs용-useurlencode-훅)
|
|
64
|
+
- [순수 JS 사용을 위한 `encodeState` 및 `decodeState`](#순수-js-사용을-위한-encodestate-및-decodestate-헬퍼)
|
|
65
|
+
- [URL과 상태 자동 동기화](#상태-자동-동기화)
|
|
66
|
+
- [저수준 `encode` 및 `decode` 함수](#encode-및-decode-헬퍼)
|
|
67
|
+
- [모범 사례](#모범-사례)
|
|
68
|
+
- [주의사항](#주의사항)
|
|
69
|
+
- [연락 및 지원](#연락-및-지원)
|
|
70
|
+
- [변경 로그](#변경-로그)
|
|
71
|
+
- [라이선스](#라이선스)
|
|
72
|
+
- [영감](#영감)
|
|
73
|
+
|
|
74
|
+
## 설치
|
|
75
|
+
|
|
76
|
+
### 1. 패키지 설치
|
|
77
|
+
|
|
78
|
+
```sh
|
|
79
|
+
# npm
|
|
80
|
+
npm install --save state-in-url
|
|
81
|
+
# yarn
|
|
82
|
+
yarn add state-in-url
|
|
83
|
+
# pnpm
|
|
84
|
+
pnpm add state-in-url
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### 2. tsconfig.json 편집
|
|
88
|
+
|
|
89
|
+
`tsconfig.json`에서 `"moduleResolution": "Node16"` 또는 `"moduleResolution": "NodeNext"`로 설정
|
|
90
|
+
|
|
91
|
+
## Next.js용 useUrlState 훅
|
|
92
|
+
|
|
93
|
+
[문서](packages/urlstate/useUrlState/README.md)
|
|
94
|
+
|
|
95
|
+
`useUrlState`는 Next.js 애플리케이션을 위한 커스텀 React 훅으로, 클라이언트 컴포넌트 간 통신을 쉽게 만듭니다. 복잡한 상태를 공유하고 URL 검색 매개변수와 동기화할 수 있게 해주며, 페이지 새로고침 시 상태를 유지하고 URL을 통해 애플리케이션 상태를 공유하는 방법을 제공합니다.
|
|
96
|
+
|
|
97
|
+
### 사용 예시
|
|
98
|
+
|
|
99
|
+
#### 기본
|
|
100
|
+
|
|
101
|
+
1. 상태 형태 정의
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
// countState.ts
|
|
105
|
+
// 상태 형태는 상수에 저장되어야 하며, 객체를 직접 전달하지 마세요
|
|
106
|
+
export const countState: CountState = { count: 0 }
|
|
107
|
+
|
|
108
|
+
type CountState = { count: number }
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
2. 임포트 및 사용
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
'use client'
|
|
115
|
+
import { useUrlState } from 'state-in-url';
|
|
116
|
+
|
|
117
|
+
import { countState } from './countState';
|
|
118
|
+
|
|
119
|
+
function MyComponent() {
|
|
120
|
+
// 서버 컴포넌트에서 searchParams 사용
|
|
121
|
+
// 예: export default async function Home({ searchParams }: { searchParams: object }) {
|
|
122
|
+
// const { state, updateState, updateUrl } = useUrlState(countState, searchParams);
|
|
123
|
+
const { state, updateState, updateUrl } = useUrlState(countState);
|
|
124
|
+
|
|
125
|
+
// 실수로 상태를 직접 변경하지 못하게 합니다, TS 필요
|
|
126
|
+
// state.count = 2 // <- 오류
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<div>
|
|
130
|
+
<p>카운트: {state.count}</p>
|
|
131
|
+
|
|
132
|
+
<button onClick={() => updateUrl({ count: state.count + 1 }), { replace: true }}>
|
|
133
|
+
증가 (URL 업데이트)
|
|
134
|
+
</button>
|
|
135
|
+
|
|
136
|
+
// React.useState와 동일한 API
|
|
137
|
+
<button onClick={() => updateState(currState => ({...currState, count: currState.count + 1 }) )}>
|
|
138
|
+
증가 (로컬만)
|
|
139
|
+
</button>
|
|
140
|
+
<button onClick={() => updateUrl()}>
|
|
141
|
+
변경사항을 URL에 동기화
|
|
142
|
+
// 또는 동기화하지 않고 상태만 공유
|
|
143
|
+
</button>
|
|
144
|
+
|
|
145
|
+
<button onClick={() => updateUrl(state)}>
|
|
146
|
+
리셋
|
|
147
|
+
</button>
|
|
148
|
+
</div>
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
#### 복잡한 상태 형태 예시
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
interface UserSettings {
|
|
157
|
+
theme: 'light' | 'dark';
|
|
158
|
+
fontSize: number;
|
|
159
|
+
notifications: boolean;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export const userSettings: UserSettings {
|
|
163
|
+
theme: 'light',
|
|
164
|
+
fontSize: 16,
|
|
165
|
+
notifications: true,
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
'use client'
|
|
171
|
+
import { useUrlState } from 'state-in-url';
|
|
172
|
+
|
|
173
|
+
import { userSettings } from './userSettings';
|
|
174
|
+
|
|
175
|
+
function SettingsComponent() {
|
|
176
|
+
// `state`는 UserSettings 타입에서 추론됩니다!
|
|
177
|
+
const { state, updateUrl } = useUrlState(userSettings);
|
|
178
|
+
|
|
179
|
+
const toggleTheme = () => {
|
|
180
|
+
updateUrl(current => ({
|
|
181
|
+
...current,
|
|
182
|
+
theme: current.theme === 'light' ? 'dark' : 'light',
|
|
183
|
+
}));
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// 유휴 상태일 때 상태를 URL에 동기화
|
|
187
|
+
const timer = React.useRef(0 as unknown as NodeJS.Timeout);
|
|
188
|
+
React.useEffect(() => {
|
|
189
|
+
clearTimeout(timer.current);
|
|
190
|
+
timer.current = setTimeout(() => {
|
|
191
|
+
// 참조가 아닌 내용으로 상태를 비교하고 새 값에 대해서만 업데이트를 실행합니다
|
|
192
|
+
updateUrl(state);
|
|
193
|
+
}, 500);
|
|
194
|
+
|
|
195
|
+
return () => {
|
|
196
|
+
clearTimeout(timer.current);
|
|
197
|
+
};
|
|
198
|
+
}, [state, updateUrl]);
|
|
199
|
+
|
|
200
|
+
return (
|
|
201
|
+
<div>
|
|
202
|
+
<h2>사용자 설정</h2>
|
|
203
|
+
<p>테마: {state.theme}</p>
|
|
204
|
+
<p>글꼴 크기: {state.fontSize}px</p>
|
|
205
|
+
<button onClick={toggleTheme}>테마 전환</button>
|
|
206
|
+
{/* 다른 설정을 업데이트하기 위한 UI 요소들 */}
|
|
207
|
+
</div>
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
...
|
|
211
|
+
|
|
212
|
+
// 다른 컴포넌트
|
|
213
|
+
function Component() {
|
|
214
|
+
const { state } = useUrlState(defaultSettings);
|
|
215
|
+
|
|
216
|
+
return (
|
|
217
|
+
<div>
|
|
218
|
+
<p>알림은 {state.notifications ? '켜짐' : '꺼짐'}</p>
|
|
219
|
+
</div>
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
#### 상태 자동 동기화
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
const timer = React.useRef(0 as unknown as NodeJS.Timeout);
|
|
228
|
+
React.useEffect(() => {
|
|
229
|
+
clearTimeout(timer.current);
|
|
230
|
+
timer.current = setTimeout(() => {
|
|
231
|
+
// 참조가 아닌 내용으로 상태를 비교하고 새 값에 대해서만 업데이트를 실행합니다
|
|
232
|
+
updateUrl(state);
|
|
233
|
+
}, 500);
|
|
234
|
+
|
|
235
|
+
return () => {
|
|
236
|
+
clearTimeout(timer.current);
|
|
237
|
+
};
|
|
238
|
+
}, [state, updateUrl]);
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
#### 임의의 상태 형태 (권장하지 않음)
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
'use client'
|
|
245
|
+
import { useUrlState } from 'state-in-url';
|
|
246
|
+
|
|
247
|
+
const someObj = {};
|
|
248
|
+
|
|
249
|
+
function SettingsComponent() {
|
|
250
|
+
const { state, updateUrl, updateState } = useUrlState<object>(someObj);
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## React.js용 `useUrlEncode` 훅
|
|
255
|
+
|
|
256
|
+
[문서](packages/urlstate/useUrlEncode/README.md)
|
|
257
|
+
|
|
258
|
+
## 순수 JS 사용을 위한 `encodeState` 및 `decodeState` 헬퍼
|
|
259
|
+
|
|
260
|
+
[문서](packages/urlstate/encodeState/README.md)
|
|
261
|
+
|
|
262
|
+
## `encode` 및 `decode` 헬퍼
|
|
263
|
+
|
|
264
|
+
[문서](packages/urlstate/encoder/README.md)
|
|
265
|
+
|
|
266
|
+
## 모범 사례
|
|
267
|
+
|
|
268
|
+
- 일관성을 보장하기 위해 상태 형태를 상수로 정의하세요
|
|
269
|
+
- 향상된 타입 안전성과 자동 완성을 위해 TypeScript를 사용하세요
|
|
270
|
+
- URL 매개변수에 민감한 정보를 저장하지 마세요
|
|
271
|
+
- 빈번한 업데이트에는 `updateState`를 사용하고, 변경 사항을 URL에 동기화하려면 `updateUrl`을 사용하세요
|
|
272
|
+
- 읽기 쉬운 TS 오류를 위해 이 [확장 프로그램](https://marketplace.visualstudio.com/items?itemName=yoavbls.pretty-ts-errors)을 사용하세요
|
|
273
|
+
|
|
274
|
+
## 주의사항
|
|
275
|
+
|
|
276
|
+
1. 직렬화 가능한 값만 전달할 수 있습니다. `Function`, `BigInt` 또는 `Symbol`은 작동하지 않으며, `ArrayBuffer`와 같은 것들도 마찬가지일 것입니다.
|
|
277
|
+
2. Vercel 서버는 헤더 크기(쿼리 문자열 및 기타 내용)를 **14KB**로 제한하므로 URL 상태를 약 5000단어 이하로 유지하세요. https://vercel.com/docs/errors/URL_TOO_LONG
|
|
278
|
+
|
|
279
|
+
## 로컬에서 실행
|
|
280
|
+
|
|
281
|
+
이 저장소를 복제하고, `npm install`을 실행한 다음
|
|
282
|
+
|
|
283
|
+
```sh
|
|
284
|
+
npm run dev
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
[localhost:3000](http://localhost:3000)으로 이동하세요
|
|
288
|
+
|
|
289
|
+
## 연락 및 지원
|
|
290
|
+
|
|
291
|
+
- 버그 보고, 기능 요청 또는 질문을 위해 [GitHub 이슈](https://github.com/asmyshlyaev177/state-in-url/issues)를 생성하세요
|
|
292
|
+
|
|
293
|
+
## [변경 로그](https://github.com/asmyshlyaev177/state-in-url/blob/main/CHANGELOG.md)
|
|
294
|
+
|
|
295
|
+
## 라이선스
|
|
296
|
+
|
|
297
|
+
이 프로젝트는 [MIT 라이선스](https://github.com/asmyshlyaev177/state-in-url/blob/main/LICENSE)에 따라 라이선스가 부여됩니다.
|
|
298
|
+
|
|
299
|
+
## 영감
|
|
300
|
+
|
|
301
|
+
[Vue에서 URL을 사용하여 상태 저장하기](https://dev.to/jacobandrewsky/using-url-to-store-state-in-vue-275c)
|
|
302
|
+
|
|
303
|
+
[URL에 상태 저장하기](https://antonz.org/storing-state/)
|
|
304
|
+
|
|
305
|
+
[NextJS useSearchParams](https://nextjs.org/docs/app/api-reference/functions/use-search-params)
|
package/README.md
CHANGED
package/dist/index.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,"__esModule",{value:!0});var t=require("react"),e=require("next/navigation");function n(t){return t&&"object"==typeof t&&"default"in t?t:{default:t}}var r=n(t);const c={string:"◖",boolean:"🗵",null:"∙null",undefined:"∙undefined",number:"∓",date:"⏲"},o=t=>{const e=null===t,n=Array.isArray(t),r=t instanceof Date;return(e?"null":r&&"date")||n&&"array"||!e&&!r&&!n&&"object"==typeof t&&"object"||typeof t},s=()=>"undefined"==typeof window,u=t=>new URLSearchParams("string"==typeof t?t:t?.toString?.()||""),a=(t,e)=>JSON.stringify(t)===JSON.stringify(e);function i(t){switch(o(t)){case"function":case"symbol":return"";case"date":return c.date+t.toISOString();case"string":return`${c.string}${encodeURIComponent(t)}`;case"number":return c.number+String(t);case"boolean":return c.boolean+String(t);case"object":case"array":return JSON.stringify(t,f).replaceAll('"',"'");case"null":return c.null;case"undefined":return c.undefined;default:return String(t)}}function l(t,e){return function(t,e){try{return JSON.parse(t,g)}catch{const n=d(t);return n!==p?n:e}}(t.replaceAll("'",'"'),e)}const f=(t,e)=>{const n=o(e);return t&&"object"!==n&&"array"!==n?i(e):e},d=t=>t===c.null?null:t!==c.undefined?t?.startsWith?.(c.number)?Number.parseFloat(t.replace(c.number,"")):t?.startsWith?.(c.boolean)?!!t.includes("true"):t?.startsWith?.(c.date)?new Date(t.slice(1)):t?.startsWith?.(c.string)?decodeURIComponent(t).replace(/^◖/,""):p:void 0,p=Symbol("isError"),g=(t,e)=>{const n="string"==typeof e,r=n&&d(e);return r===p?e:t&&n?r:e};const y=(t,e)=>{const n="string"==typeof e,r=n&&l(e?.replaceAll?.("'",'"'));return t&&n?r:e};function b(t,e){try{return JSON.parse(t,y)}catch{const n=d(t);return n!==p?n:e}}function S(t,e,n){const r=u(n);return Object.entries(t||{}).forEach((([t,n])=>{const c=e?.[t];JSON.stringify(n)!==JSON.stringify(c)&&r.set(t,i(n))})),r.toString()}function w(t,e){return{...e||{},...Object.fromEntries([...u(t).entries()].map((([t,n])=>{const r=e?.[t];return[t,l(n,r)??r]})))}}function h(t){const e=r.default.useCallback((function(e,n){return"object"===o(e)?S(e,t,n):""}),[t]);return{parse:r.default.useCallback((function(e){return w(e,t)}),[t]),stringify:e}}const m=new WeakMap,O=new WeakMap,j={get:function(t){return m.get(t)},set:function(t,e){m.set(t,e)}};exports.decode=l,exports.decodeState=w,exports.encode=i,exports.encodeState=S,exports.useUrlEncode=h,exports.useUrlState=function(t,n){const{state:c,getState:o,setState:u,stringify:i}=function(t){const e=r.default.useRef(t),{parse:n,stringify:c}=h(e.current),[o,u]=r.default.useState((()=>{const t=j.get(e.current),r=n(s()?"":window.location.search);return t||(j.set(e.current,r),r)})),i=r.default.useCallback((t=>{const n=j.get(e.current)||e.current;if("function"==typeof t){const r=t(n);if(a(n,r))return;j.set(e.current,r),(O.get(e.current)||[]).forEach((t=>{t()}))}else{if(a(n,t))return;j.set(e.current,t),(O.get(e.current)||[]).forEach((t=>{t()}))}}),[]);return r.default.useInsertionEffect((()=>{const t=O.get(e.current)||[],r=()=>{u(j.get(e.current)||e.current)};O.set(e.current,t.concat(r));const c=()=>{const t=n(window.location.search);i(t)},o="popstate";return window.addEventListener(o,c),()=>{const t=O.get(e.current)||[];O.set(e.current,t.filter((t=>t!==r))),window.removeEventListener(o,c)}}),[i]),{state:o,getState:r.default.useCallback((()=>j.get(e.current)||e.current),[]),setState:i,stringify:c}}(s()?
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:!0});var t=require("react"),e=require("next/navigation");function n(t){return t&&"object"==typeof t&&"default"in t?t:{default:t}}var r=n(t);const c={string:"◖",boolean:"🗵",null:"∙null",undefined:"∙undefined",number:"∓",date:"⏲"},o=t=>{const e=null===t,n=Array.isArray(t),r=t instanceof Date;return(e?"null":r&&"date")||n&&"array"||!e&&!r&&!n&&"object"==typeof t&&"object"||typeof t},s=()=>"undefined"==typeof window,u=t=>new URLSearchParams("string"==typeof t?t:t?.toString?.()||""),a=(t,e)=>JSON.stringify(t)===JSON.stringify(e);function i(t){switch(o(t)){case"function":case"symbol":return"";case"date":return c.date+t.toISOString();case"string":return`${c.string}${encodeURIComponent(t)}`;case"number":return c.number+String(t);case"boolean":return c.boolean+String(t);case"object":case"array":return JSON.stringify(t,f).replaceAll('"',"'");case"null":return c.null;case"undefined":return c.undefined;default:return String(t)}}function l(t,e){return function(t,e){try{return JSON.parse(t,g)}catch{const n=d(t);return n!==p?n:e}}(t.replaceAll("'",'"'),e)}const f=(t,e)=>{const n=o(e);return t&&"object"!==n&&"array"!==n?i(e):e},d=t=>t===c.null?null:t!==c.undefined?t?.startsWith?.(c.number)?Number.parseFloat(t.replace(c.number,"")):t?.startsWith?.(c.boolean)?!!t.includes("true"):t?.startsWith?.(c.date)?new Date(t.slice(1)):t?.startsWith?.(c.string)?decodeURIComponent(t).replace(/^◖/,""):p:void 0,p=Symbol("isError"),g=(t,e)=>{const n="string"==typeof e,r=n&&d(e);return r===p?e:t&&n?r:e};const y=(t,e)=>{const n="string"==typeof e,r=n&&l(e?.replaceAll?.("'",'"'));return t&&n?r:e};function b(t,e){try{return JSON.parse(t,y)}catch{const n=d(t);return n!==p?n:e}}function S(t,e,n){const r=u(n);return Object.entries(t||{}).forEach((([t,n])=>{const c=e?.[t];JSON.stringify(n)!==JSON.stringify(c)&&r.set(t,i(n))})),r.toString()}function w(t,e){return{...e||{},...Object.fromEntries([...u(t).entries()].map((([t,n])=>{const r=e?.[t];return[t,l(n,r)??r]})))}}function h(t){const e=r.default.useCallback((function(e,n){return"object"===o(e)?S(e,t,n):""}),[t]);return{parse:r.default.useCallback((function(e){return w(e,t)}),[t]),stringify:e}}const m=new WeakMap,O=new WeakMap,j={get:function(t){return m.get(t)},set:function(t,e){m.set(t,e)}};exports.decode=l,exports.decodeState=w,exports.encode=i,exports.encodeState=S,exports.useUrlEncode=h,exports.useUrlState=function(t,n){const{state:c,getState:o,setState:u,stringify:i}=function(t){const e=r.default.useRef(t),{parse:n,stringify:c}=h(e.current),[o,u]=r.default.useState((()=>{const t=j.get(e.current),r=n(s()?"":window.location.search);return t||(j.set(e.current,r),r)})),i=r.default.useCallback((t=>{const n=j.get(e.current)||e.current;if("function"==typeof t){const r=t(n);if(a(n,r))return;j.set(e.current,r),(O.get(e.current)||[]).forEach((t=>{t()}))}else{if(a(n,t))return;j.set(e.current,t),(O.get(e.current)||[]).forEach((t=>{t()}))}}),[]);return r.default.useInsertionEffect((()=>{const t=O.get(e.current)||[],r=()=>{u(j.get(e.current)||e.current)};O.set(e.current,t.concat(r));const c=()=>{const t=n(window.location.search);i(t)},o="popstate";return window.addEventListener(o,c),()=>{const t=O.get(e.current)||[];O.set(e.current,t.filter((t=>t!==r))),window.removeEventListener(o,c)}}),[i]),{state:o,getState:r.default.useCallback((()=>j.get(e.current)||e.current),[]),setState:i,stringify:c}}(s()?(l=n,f=t,{...f,...b(JSON.stringify(l),f)}):t);var l,f;const d=e.useRouter(),p=r.default.useCallback(((t,e)=>{const n=window.location.search,r=`${window.location.pathname}${n.length&&!n.includes("?")?"?":""}${n}`;let c,s;"function"==typeof t?(c=t(o()),s=i(c)):(c=t??o(),s=i(c)),u(c);const a=`${window.location.pathname}${s.length?"?":""}${s}`;if(r!==a){const{replace:t,...n}=e||{};d[t?"replace":"push"](a,{scroll:!1,...n})}}),[d,i,o]);return{updateState:u,updateUrl:p,state:c}};
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import t from"react";import{useRouter as e}from"next/navigation";const n={string:"◖",boolean:"🗵",null:"∙null",undefined:"∙undefined",number:"∓",date:"⏲"},r=t=>{const e=null===t,n=Array.isArray(t),r=t instanceof Date;return(e?"null":r&&"date")||n&&"array"||!e&&!r&&!n&&"object"==typeof t&&"object"||typeof t},c=()=>"undefined"==typeof window,o=t=>new URLSearchParams("string"==typeof t?t:t?.toString?.()||""),s=(t,e)=>JSON.stringify(t)===JSON.stringify(e);function
|
|
1
|
+
import t from"react";import{useRouter as e}from"next/navigation";const n={string:"◖",boolean:"🗵",null:"∙null",undefined:"∙undefined",number:"∓",date:"⏲"},r=t=>{const e=null===t,n=Array.isArray(t),r=t instanceof Date;return(e?"null":r&&"date")||n&&"array"||!e&&!r&&!n&&"object"==typeof t&&"object"||typeof t},c=()=>"undefined"==typeof window,o=t=>new URLSearchParams("string"==typeof t?t:t?.toString?.()||""),s=(t,e)=>JSON.stringify(t)===JSON.stringify(e);function a(t){switch(r(t)){case"function":case"symbol":return"";case"date":return n.date+t.toISOString();case"string":return`${n.string}${encodeURIComponent(t)}`;case"number":return n.number+String(t);case"boolean":return n.boolean+String(t);case"object":case"array":return JSON.stringify(t,i).replaceAll('"',"'");case"null":return n.null;case"undefined":return n.undefined;default:return String(t)}}function u(t,e){return function(t,e){try{return JSON.parse(t,g)}catch{const n=l(t);return n!==f?n:e}}(t.replaceAll("'",'"'),e)}const i=(t,e)=>{const n=r(e);return t&&"object"!==n&&"array"!==n?a(e):e},l=t=>t===n.null?null:t!==n.undefined?t?.startsWith?.(n.number)?Number.parseFloat(t.replace(n.number,"")):t?.startsWith?.(n.boolean)?!!t.includes("true"):t?.startsWith?.(n.date)?new Date(t.slice(1)):t?.startsWith?.(n.string)?decodeURIComponent(t).replace(/^◖/,""):f:void 0,f=Symbol("isError"),g=(t,e)=>{const n="string"==typeof e,r=n&&l(e);return r===f?e:t&&n?r:e};const d=(t,e)=>{const n="string"==typeof e,r=n&&u(e?.replaceAll?.("'",'"'));return t&&n?r:e};function p(t,e){try{return JSON.parse(t,d)}catch{const n=l(t);return n!==f?n:e}}function y(t,e,n){const r=o(n);return Object.entries(t||{}).forEach((([t,n])=>{const c=e?.[t];JSON.stringify(n)!==JSON.stringify(c)&&r.set(t,a(n))})),r.toString()}function b(t,e){return{...e||{},...Object.fromEntries([...o(t).entries()].map((([t,n])=>{const r=e?.[t];return[t,u(n,r)??r]})))}}function S(e){const n=t.useCallback((function(t,n){return"object"===r(t)?y(t,e,n):""}),[e]);return{parse:t.useCallback((function(t){return b(t,e)}),[e]),stringify:n}}const w=new WeakMap,m=new WeakMap,h={get:function(t){return w.get(t)},set:function(t,e){w.set(t,e)}};function O(n,r){const{state:o,getState:a,setState:u,stringify:i}=function(e){const n=t.useRef(e),{parse:r,stringify:o}=S(n.current),[a,u]=t.useState((()=>{const t=h.get(n.current),e=r(c()?"":window.location.search);return t||(h.set(n.current,e),e)})),i=t.useCallback((t=>{const e=h.get(n.current)||n.current;if("function"==typeof t){const r=t(e);if(s(e,r))return;h.set(n.current,r),(m.get(n.current)||[]).forEach((t=>{t()}))}else{if(s(e,t))return;h.set(n.current,t),(m.get(n.current)||[]).forEach((t=>{t()}))}}),[]);return t.useInsertionEffect((()=>{const t=m.get(n.current)||[],e=()=>{u(h.get(n.current)||n.current)};m.set(n.current,t.concat(e));const c=()=>{const t=r(window.location.search);i(t)},o="popstate";return window.addEventListener(o,c),()=>{const t=m.get(n.current)||[];m.set(n.current,t.filter((t=>t!==e))),window.removeEventListener(o,c)}}),[i]),{state:a,getState:t.useCallback((()=>h.get(n.current)||n.current),[]),setState:i,stringify:o}}(c()?(l=r,f=n,{...f,...p(JSON.stringify(l),f)}):n);var l,f;const g=e(),d=t.useCallback(((t,e)=>{const n=window.location.search,r=`${window.location.pathname}${n.length&&!n.includes("?")?"?":""}${n}`;let c,o;"function"==typeof t?(c=t(a()),o=i(c)):(c=t??a(),o=i(c)),u(c);const s=`${window.location.pathname}${o.length?"?":""}${o}`;if(r!==s){const{replace:t,...n}=e||{};g[t?"replace":"push"](s,{scroll:!1,...n})}}),[g,i,a]);return{updateState:u,updateUrl:d,state:o}}export{u as decode,b as decodeState,a as encode,y as encodeState,S as useUrlEncode,O as useUrlState};
|
|
@@ -18,7 +18,7 @@ import { type DeepReadonly, type JSONCompatible } from '../utils';
|
|
|
18
18
|
*
|
|
19
19
|
* * Github {@link https://github.com/asmyshlyaev177/state-in-url/tree/main/packages/urlstate/useUrlState#api}
|
|
20
20
|
*/
|
|
21
|
-
export declare function useUrlState<T extends JSONCompatible>(defaultState: T,
|
|
21
|
+
export declare function useUrlState<T extends JSONCompatible>(defaultState: T, searchParams?: object): {
|
|
22
22
|
/**
|
|
23
23
|
* * Example:
|
|
24
24
|
* ```ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "state-in-url",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Library to store state in browser URL, includes hook for NextJS, hook for pure React, and low level helpers.",
|
|
5
5
|
"homepage": "https://github.com/asmyshlyaev177/state-in-url#readme",
|
|
6
6
|
"keywords": [
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"email": "asmyshlyaev177@gmail.com",
|
|
27
27
|
"url": "https://www.linkedin.com/in/asmyshlyaev177/"
|
|
28
28
|
},
|
|
29
|
-
"license": "
|
|
29
|
+
"license": "MIT",
|
|
30
30
|
"funding": {
|
|
31
31
|
"type": "patreon",
|
|
32
32
|
"url": "https://patreon.com/asmyshlyaev177"
|