state-in-url 0.2.7 → 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 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
+ [![npm](https://img.shields.io/npm/v/state-in-url.svg)](https://www.npmjs.com/package/state-in-url)
15
+ ![Tests](https://github.com/asmyshlyaev177/state-in-url/actions/workflows/tests.yml/badge.svg?branch=main)
16
+ [![Codacy Badge](https://app.codacy.com/project/badge/Grade/73be54068b7f41b0b74a252579ac09ec)](https://app.codacy.com/gh/asmyshlyaev177/state-in-url/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
17
+ [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](https://github.com/asmyshlyaev177/state-in-url/)
18
+ ![npm bundle size (minified + gzip)](https://img.shields.io/bundlephobia/minzip/state-in-url.svg)
19
+ </div>
20
+
21
+ <div align="center">
22
+
23
+ <h4 align="center">如果发现bug,请不要犹豫,立即提出问题</h4>
24
+
25
+ ![演示-gif](https://github.com/asmyshlyaev177/state-in-url/assets/19854148/c9802601-4d42-4362-b3e4-37ff87c3b97f)
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
+ [![npm](https://img.shields.io/npm/v/state-in-url.svg)](https://www.npmjs.com/package/state-in-url)
15
+ ![Tests](https://github.com/asmyshlyaev177/state-in-url/actions/workflows/tests.yml/badge.svg?branch=main)
16
+ [![Codacy Badge](https://app.codacy.com/project/badge/Grade/73be54068b7f41b0b74a252579ac09ec)](https://app.codacy.com/gh/asmyshlyaev177/state-in-url/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
17
+ [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](https://github.com/asmyshlyaev177/state-in-url/)
18
+ ![npm bundle size (minified + gzip)](https://img.shields.io/bundlephobia/minzip/state-in-url.svg)
19
+ </div>
20
+
21
+ <div align="center">
22
+
23
+ <h4 align="center">버그를 발견하셨다면 주저하지 마시고 이슈를 열어주세요</h4>
24
+
25
+ ![데모-gif](https://github.com/asmyshlyaev177/state-in-url/assets/19854148/c9802601-4d42-4362-b3e4-37ff87c3b97f)
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
@@ -1,3 +1,4 @@
1
+ English | [한국어](./README.KO.md) | [简体中文](./README.CN.md)
1
2
 
2
3
  <h1 align="center">state-in-url</h1>
3
4
 
@@ -28,7 +29,6 @@
28
29
 
29
30
 
30
31
  <a href="https://state-in-url-asmyshlyaev177.vercel.app/" target="_blank">DEMO</a> |
31
- <a href="https://stackblitz.com/~/github.com/asmyshlyaev177/state-in-url?file=packages%2Fexample-nextjs%2Fsrc%2Fapp%2FForm.tsx" target="_blank">Demo on Stackblitz</a> |
32
32
  <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>
33
33
 
34
34
 
@@ -275,6 +275,7 @@ function SettingsComponent() {
275
275
  - Use TypeScript for enhanced type safety and autocomplete
276
276
  - Avoid storing sensitive information in URL parameters
277
277
  - Use `updateState` for frequent updates and `updateUrl` to sync changes to url
278
+ - Use this [extension](https://marketplace.visualstudio.com/items?itemName=yoavbls.pretty-ts-errors) for readable TS errors
278
279
 
279
280
  ## Gothas
280
281
 
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 t.match(/^◖/)?encodeURIComponent(t):`${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()?function(t,e){return{...e,...b(JSON.stringify(t),e)}}(n,t):t),l=e.useRouter(),f=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||{};l[t?"replace":"push"](a,{scroll:!1,...n})}}),[l,i,o]);return{updateState:u,updateUrl:f,state:c}};
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 a(t){switch(r(t)){case"function":case"symbol":return"";case"date":return n.date+t.toISOString();case"string":return t.match(/^◖/)?encodeURIComponent(t):`${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 m=new WeakMap,w=new WeakMap,h={get:function(t){return m.get(t)},set:function(t,e){m.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),(w.get(n.current)||[]).forEach((t=>{t()}))}else{if(s(e,t))return;h.set(n.current,t),(w.get(n.current)||[]).forEach((t=>{t()}))}}),[]);return t.useInsertionEffect((()=>{const t=w.get(n.current)||[],e=()=>{u(h.get(n.current)||n.current)};w.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=w.get(n.current)||[];w.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()?function(t,e){return{...e,...p(JSON.stringify(t),e)}}(r,n):n),l=e(),f=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||{};l[t?"replace":"push"](s,{scroll:!1,...n})}}),[l,i,a]);return{updateState:u,updateUrl:f,state:o}}export{u as decode,b as decodeState,a as encode,y as encodeState,S as useUrlEncode,O as useUrlState};
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};
@@ -1,7 +1,7 @@
1
- import { type JSONCompatible } from './utils';
1
+ import { type DeepReadonly, type JSONCompatible } from './utils';
2
2
  export declare function useState<T extends JSONCompatible>(defaultState: T): {
3
3
  state: T;
4
4
  getState: () => T;
5
- setState: (value: T | ((currState: T) => T)) => void;
5
+ setState: (value: T | DeepReadonly<T> | ((currState: T) => T)) => void;
6
6
  stringify: (state: T, paramsToKeep?: string | URLSearchParams | undefined) => string;
7
7
  };
@@ -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, sp?: object): {
21
+ export declare function useUrlState<T extends JSONCompatible>(defaultState: T, searchParams?: object): {
22
22
  /**
23
23
  * * Example:
24
24
  * ```ts
@@ -29,7 +29,7 @@ export declare function useUrlState<T extends JSONCompatible>(defaultState: T, s
29
29
  *
30
30
  * * Github {@link https://github.com/asmyshlyaev177/state-in-url/tree/main/packages/urlstate/useUrlState#updatestate}
31
31
  */
32
- updateState: (value: T | ((currState: T) => T)) => void;
32
+ updateState: (value: T | DeepReadonly<T> | ((currState: T) => T)) => void;
33
33
  /**
34
34
  * * Example:
35
35
  * ```ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "state-in-url",
3
- "version": "0.2.7",
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": "ISC",
29
+ "license": "MIT",
30
30
  "funding": {
31
31
  "type": "patreon",
32
32
  "url": "https://patreon.com/asmyshlyaev177"