state-in-url 1.2.0 → 1.3.0-beta.2

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.
Files changed (34) hide show
  1. package/README.CN.md +146 -64
  2. package/README.md +73 -4
  3. package/dist/constants/constants.mjs +1 -0
  4. package/dist/encodeState/encodeState.mjs +1 -0
  5. package/dist/{urlstate/encoder → encoder}/encoder.d.ts +7 -0
  6. package/dist/encoder/encoder.mjs +1 -0
  7. package/dist/index.d.ts +9 -1
  8. package/dist/index.mjs +1 -1
  9. package/dist/{urlstate → next}/useUrlState/useUrlState.d.ts +9 -6
  10. package/dist/next/useUrlState/useUrlState.mjs +1 -0
  11. package/dist/subscribers.mjs +1 -0
  12. package/dist/useInsertionEffect.mjs +1 -0
  13. package/dist/useSharedState/useSharedState.mjs +1 -0
  14. package/dist/useUrlEncode/useUrlEncode.mjs +1 -0
  15. package/dist/useUrlStateBase/index.d.ts +1 -0
  16. package/dist/useUrlStateBase/useUrlStateBase.d.ts +31 -0
  17. package/dist/useUrlStateBase/useUrlStateBase.mjs +1 -0
  18. package/dist/utils.mjs +1 -0
  19. package/package.json +26 -18
  20. package/dist/index.cjs +0 -1
  21. package/dist/urlstate/index.d.ts +0 -8
  22. /package/dist/{urlstate/constants → constants}/constants.d.ts +0 -0
  23. /package/dist/{urlstate/constants → constants}/index.d.ts +0 -0
  24. /package/dist/{urlstate/encodeState → encodeState}/encodeState.d.ts +0 -0
  25. /package/dist/{urlstate/encodeState → encodeState}/index.d.ts +0 -0
  26. /package/dist/{urlstate/encoder → encoder}/index.d.ts +0 -0
  27. /package/dist/{urlstate → next}/useUrlState/index.d.ts +0 -0
  28. /package/dist/{urlstate/subscribers.d.ts → subscribers.d.ts} +0 -0
  29. /package/dist/{urlstate/useInsertionEffect.d.ts → useInsertionEffect.d.ts} +0 -0
  30. /package/dist/{urlstate/useSharedState → useSharedState}/index.d.ts +0 -0
  31. /package/dist/{urlstate/useSharedState → useSharedState}/useSharedState.d.ts +0 -0
  32. /package/dist/{urlstate/useUrlEncode → useUrlEncode}/index.d.ts +0 -0
  33. /package/dist/{urlstate/useUrlEncode → useUrlEncode}/useUrlEncode.d.ts +0 -0
  34. /package/dist/{urlstate/utils.d.ts → utils.d.ts} +0 -0
package/README.CN.md CHANGED
@@ -1,11 +1,9 @@
1
- [English](./README.md) | [한국어](./README.KO.md) | 简体中文
2
-
3
1
  <h1 align="center">state-in-url</h1>
4
2
 
5
3
  <div align="center">
6
4
  <img src="/assets/logo.svg?raw=true" alt="库标志"/>
7
5
 
8
- <div>轻松在不相关的 React 组件之间共享状态,具有 IDE 自动完成和 TS 验证功能。无需任何麻烦或样板代码。</div>
6
+ <div>轻松在不相关的React组件之间共享状态,具有IDE自动完成和TS验证功能。无需任何麻烦或样板代码。</div>
9
7
  </div>
10
8
 
11
9
  <div align="center">
@@ -20,50 +18,51 @@
20
18
 
21
19
  <div align="center">
22
20
 
23
- <h4 align="center">如果发现bug,请不要犹豫,立即提出问题</h4>
21
+ <h4 align="center">如果发现bug,请随时提出issue</h4>
24
22
 
25
23
  ![演示-gif](https://github.com/asmyshlyaev177/state-in-url/assets/19854148/c9802601-4d42-4362-b3e4-37ff87c3b97f)
26
24
 
27
- <a href="https://state-in-url-asmyshlyaev177.vercel.app/" target="_blank">DEMO</a> |
25
+ <a href="https://state-in-url-asmyshlyaev177.vercel.app/" target="_blank">演示</a> |
28
26
  <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
27
 
30
28
  <hr />
31
29
 
32
- 给项目加个 ⭐️ 以示支持!
30
+ 添加⭐️来支持项目!
33
31
 
34
32
  <hr />
35
33
 
36
34
  </div>
37
35
 
38
- ## 为什么使用 state-in-url?
36
+ ## 为什么使用state-in-url?
39
37
 
40
- `state-in-url` 是一个简单的状态管理工具,具有 URL 同步功能。
38
+ `state-in-url`是一个简单的状态管理工具,可以与URL同步。在不相关的React组件之间共享复杂状态,将状态同步到URL,对TS友好,兼容NextJS。
41
39
 
42
40
  # 使用场景
43
41
 
44
- - 🙃 在不改变 URL 的情况下在不同组件间共享状态,是信号和其他状态管理工具的良好替代品
45
- - 🔗 包含完整应用程序状态的可共享 URL
46
- - 🔄 在页面重新加载时轻松保持状态
47
- - 🧠 在不相关的客户端组件间同步数据
48
- - 🧮 在 URL 中存储未保存的用户表单
42
+ - 🙃 在不改变URL的情况下,在不同组件之间共享状态,是信号和其他状态管理工具的良好替代方案
43
+ - 🔗 包含完整应用程序状态的可共享URL
44
+ - 🔄 在页面重新加载之间轻松持久化状态
45
+ - 🧠 在不相关的客户端组件之间同步数据
46
+ - 🧮 在URL中存储未保存的用户表单
49
47
 
50
48
  # 特性
51
49
 
52
- - 🧩 **简单**: 无需提供者、减速器、样板代码或新概念
53
- - 📘 **TypeScript 支持和类型安全**: 保留数据类型和结构,通过 IDE 建议、强类型和 JSDoc 注释增强开发体验
54
- - ⚛️ **框架灵活性**: 为 Next.js 和 React.js 应用程序提供单独的钩子,以及纯 JS 函数
55
- - ⚙ **测试充分**: 单元测试和 Playwright 测试
56
- - ⚡ **快速**: 最小化重新渲染
57
- - 🪶 **轻量级**: 零依赖,占用空间小
50
+ - 🧩 **简单**:无需提供者、reducers、样板代码或新概念
51
+ - 📘 **TypeScript支持和类型安全**:保留数据类型和结构,通过IDE建议、强类型和JSDoc注释增强开发体验
52
+ - ⚛️ **框架灵活性**:为Next.js和React.js应用程序提供单独的钩子,以及用于纯JS的函数
53
+ - ⚙ **经过充分测试**:单元测试和Playwright测试
54
+ - ⚡ **快速**:最少的重新渲染
55
+ - 🪶 **轻量级**:零依赖,占用空间小
58
56
 
59
57
  ## 目录
60
58
 
61
59
  - [安装](#安装)
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-辅助函数)
60
+ - [Next.js的`useUrlState`](#nextjs的useurlstate钩子)
61
+ - [React.js的`useSharedState`](#reactjs的usesharedstate钩子)
62
+ - [React.js的`useUrlEncode`](#reactjs的useurlencode钩子)
63
+ - [纯JS使用的`encodeState`和`decodeState`](#纯js使用的encodestate和decodestate辅助函数)
64
+ - [自动同步状态到URL](#自动同步状态)
65
+ - [低级`encode`和`decode`函数](#encode和decode辅助函数)
67
66
  - [最佳实践](#最佳实践)
68
67
  - [注意事项](#注意事项)
69
68
  - [联系与支持](#联系与支持)
@@ -84,15 +83,15 @@ yarn add state-in-url
84
83
  pnpm add state-in-url
85
84
  ```
86
85
 
87
- ### 2. 编辑 tsconfig.json
86
+ ### 2. 编辑tsconfig.json
88
87
 
89
- 在 `tsconfig.json` 中设置 `"moduleResolution": "Node16"` 或 `"moduleResolution": "NodeNext"`
88
+ 在`tsconfig.json`的`compilerOptions`中设置`"moduleResolution": "Node16"`或`"moduleResolution": "NodeNext"`
90
89
 
91
- ## Next.js 的 useUrlState 钩子
90
+ ## Next.js的useUrlState钩子
92
91
 
93
- [文档](packages/urlstate/useUrlState/README.md)
92
+ [文档](ackages/urlstate/next/useUrlState#api)
94
93
 
95
- `useUrlState` 是一个为 Next.js 应用程序设计的自定义 React 钩子,使客户端组件之间的通信变得简单。它允许你共享任何复杂的状态并将其与 URL 搜索参数同步,提供了一种在页面重新加载时保持状态和通过 URL 共享应用程序状态的方法。
94
+ `useUrlState`是一个为Next.js应用程序设计的自定义React钩子,使客户端组件之间的通信变得简单。它允许你共享任何复杂的状态并将其与URL搜索参数同步,提供了一种在页面重新加载之间持久化状态并通过URL共享应用程序状态的方法。
96
95
 
97
96
  ### 使用示例
98
97
 
@@ -112,33 +111,33 @@ pnpm add state-in-url
112
111
 
113
112
  ```typescript
114
113
  'use client'
115
- import { useUrlState } from 'state-in-url';
114
+ import { useUrlState } from 'state-in-url/next';
116
115
 
117
116
  import { countState } from './countState';
118
117
 
119
118
  function MyComponent() {
120
- // 用于从服务器组件使用 searchParams
119
+ // 用于从服务器组件使用searchParams
121
120
  // 例如 export default async function Home({ searchParams }: { searchParams: object }) {
122
121
  // const { state, updateState, updateUrl } = useUrlState(countState, searchParams);
123
122
  const { state, updateState, updateUrl } = useUrlState(countState);
124
123
 
125
- // 不会让你意外地直接修改状态,需要 TS
124
+ // 不允许你意外地直接修改状态,需要TS
126
125
  // state.count = 2 // <- 错误
127
126
 
128
127
  return (
129
128
  <div>
130
- <p>计数: {state.count}</p>
129
+ <p>计数:{state.count}</p>
131
130
 
132
131
  <button onClick={() => updateUrl({ count: state.count + 1 }), { replace: true }}>
133
- 增加 (更新 URL)
132
+ 增加(更新URL)
134
133
  </button>
135
134
 
136
- // 与 React.useState 相同的 API
135
+ // 与React.useState相同的API
137
136
  <button onClick={() => updateState(currState => ({...currState, count: currState.count + 1 }) )}>
138
- 增加 (仅本地)
137
+ 增加(仅本地)
139
138
  </button>
140
139
  <button onClick={() => updateUrl()}>
141
- 同步更改到 URL
140
+ 同步更改到URL
142
141
  // 或者不同步,只共享状态
143
142
  </button>
144
143
 
@@ -168,12 +167,12 @@ export const userSettings: UserSettings {
168
167
 
169
168
  ```typescript
170
169
  'use client'
171
- import { useUrlState } from 'state-in-url';
170
+ import { useUrlState } from 'state-in-url/next';
172
171
 
173
172
  import { userSettings } from './userSettings';
174
173
 
175
174
  function SettingsComponent() {
176
- // `state` 将从 UserSettings 类型推断!
175
+ // `state` 将从UserSettings类型推断!
177
176
  const { state, updateUrl } = useUrlState(userSettings);
178
177
 
179
178
  const toggleTheme = () => {
@@ -183,12 +182,12 @@ function SettingsComponent() {
183
182
  }));
184
183
  };
185
184
 
186
- // 空闲时同步状态到 url
185
+ // 空闲时同步状态到URL
187
186
  const timer = React.useRef(0 as unknown as NodeJS.Timeout);
188
187
  React.useEffect(() => {
189
188
  clearTimeout(timer.current);
190
189
  timer.current = setTimeout(() => {
191
- // 将按内容而不是引用比较状态,并仅对新值触发更新
190
+ // 将通过内容而不是引用比较状态,只为新值触发更新
192
191
  updateUrl(state);
193
192
  }, 500);
194
193
 
@@ -200,10 +199,10 @@ function SettingsComponent() {
200
199
  return (
201
200
  <div>
202
201
  <h2>用户设置</h2>
203
- <p>主题: {state.theme}</p>
204
- <p>字体大小: {state.fontSize}px</p>
202
+ <p>主题:{state.theme}</p>
203
+ <p>字体大小:{state.fontSize}px</p>
205
204
  <button onClick={toggleTheme}>切换主题</button>
206
- {/* 其他用于更新其他设置的 UI 元素 */}
205
+ {/* 其他UI元素用于更新其他设置 */}
207
206
  </div>
208
207
  );
209
208
  }
@@ -215,7 +214,7 @@ function Component() {
215
214
 
216
215
  return (
217
216
  <div>
218
- <p>通知功能 {state.notifications ? '开启' : '关闭'}</p>
217
+ <p>通知 {state.notifications ? '开启' : '关闭'}</p>
219
218
  </div>
220
219
  )
221
220
  }
@@ -228,7 +227,7 @@ function Component() {
228
227
  React.useEffect(() => {
229
228
  clearTimeout(timer.current);
230
229
  timer.current = setTimeout(() => {
231
- // 将按内容而不是引用比较状态,并仅对新值触发更新
230
+ // 将通过内容而不是引用比较状态,只为新值触发更新
232
231
  updateUrl(state);
233
232
  }, 500);
234
233
 
@@ -238,11 +237,76 @@ function Component() {
238
237
  }, [state, updateUrl]);
239
238
  ```
240
239
 
241
- #### 任意状态形状 (不推荐)
240
+ #### 服务器端渲染示例
242
241
 
243
242
  ```typescript
243
+ export default async function Home({ searchParams }: { searchParams: object }) {
244
+ return (
245
+ <Form sp={searchParams} />
246
+ )
247
+ }
248
+
249
+ // Form.tsx
244
250
  'use client'
245
- import { useUrlState } from 'state-in-url';
251
+ import React from 'react';
252
+ import { useUrlState } from 'state-in-url/next';
253
+ import { form } from './form';
254
+
255
+ const Form = ({ sp }: { sp: object }) => {
256
+ const { state, updateState, updateUrl } = useUrlState(form, sp);
257
+ }
258
+ ```
259
+
260
+ #### 在`layout`组件中使用钩子
261
+
262
+ 这是一个棘手的部分,因为使用app router的nextjs不允许从服务器端访问searchParams。有一个使用中间件的解决方法,但它并不漂亮,而且可能在nextjs更新后停止工作。
263
+
264
+ ```typescript
265
+ // 添加到适当的`layout.tsc`
266
+ export const runtime = 'edge';
267
+
268
+ // middleware.ts
269
+ import type { NextRequest } from 'next/server';
270
+ import { NextResponse } from 'next/server';
271
+
272
+ export function middleware(request: NextRequest) {
273
+ const url = request.url?.includes('_next') ? null : request.url;
274
+ const sp = url?.split?.('?')?.[1] || '';
275
+
276
+ const response = NextResponse.next();
277
+
278
+ if (url !== null) {
279
+ response.headers.set('searchParams', sp);
280
+ }
281
+
282
+ return response;
283
+ }
284
+
285
+ // 目标layout组件
286
+ import { headers } from 'next/headers';
287
+ import { decodeState } from 'state-in-url/encodeState';
288
+
289
+ export default async function Layout({
290
+ children,
291
+ }: {
292
+ children: React.ReactNode;
293
+ }) {
294
+ const sp = headers().get('searchParams') || '';
295
+
296
+ return (
297
+ <div>
298
+ <Comp1 searchParams={decodeState(sp, stateShape)} />
299
+ {children}
300
+ </div>
301
+ );
302
+ }
303
+ ```
304
+
305
+ #### 任意状态形状示例(不推荐)
306
+
307
+ ```typescript
308
+ 'use client'
309
+ import { useUrlState } from 'state-in-url/next';
246
310
 
247
311
  const someObj = {};
248
312
 
@@ -251,55 +315,73 @@ function SettingsComponent() {
251
315
  }
252
316
  ```
253
317
 
254
- ## React.js 的 `useUrlEncode` 钩子
318
+ ## React.js的`useSharedState`钩子
319
+ 用于在任何React组件之间共享状态的钩子,已在Next.js和Vite中测试。
320
+
321
+ ```typescript
322
+ 'use client'
323
+ import { useSharedState } from 'state-in-url';
324
+
325
+ export const someState = { name: '' };
326
+
327
+ function SettingsComponent() {
328
+ const { state, setState } = useSharedState(someState);
329
+ }
330
+ ```
331
+
332
+ [文档](packages/urlstate/useSharedState/README.md)
333
+
334
+ ## React.js的`useUrlEncode`钩子
255
335
 
256
336
  [文档](packages/urlstate/useUrlEncode/README.md)
257
337
 
258
- ## 纯 JS 使用的 `encodeState` 和 `decodeState` 辅助函数
338
+ ## 纯JS使用的`encodeState`和`decodeState`辅助函数
259
339
 
260
340
  [文档](packages/urlstate/encodeState/README.md)
261
341
 
262
- ## `encode` 和 `decode` 辅助函数
342
+ ## `encode`和`decode`辅助函数
263
343
 
264
344
  [文档](packages/urlstate/encoder/README.md)
265
345
 
266
346
  ## 最佳实践
267
347
 
268
- - 将状态形状定义为常量以确保一致性
269
- - 使用 TypeScript 以增强类型安全性和自动完成功能
270
- - 避免在 URL 参数中存储敏感信息
271
- - 对频繁更新使用 `updateState`,使用 `updateUrl` 同步更改到 url
272
- - 使用此[扩展](https://marketplace.visualstudio.com/items?itemName=yoavbls.pretty-ts-errors)以获得可读的 TS 错误
348
+ - 将您的状态形状定义为常量,以确保一致性
349
+ - 使用TypeScript以增强类型安全性和自动完成功能
350
+ - 避免在URL参数中存储敏感信息
351
+ - 使用`updateState`进行频繁更新,使用`updateUrl`同步更改到URL
352
+ - 在Next.js中使用`Suspence`包装客户端组件
353
+ - 使用这个[扩展](https://marketplace.visualstudio.com/items?itemName=yoavbls.pretty-ts-errors)以获得可读的TS错误
273
354
 
274
355
  ## 注意事项
275
356
 
276
- 1. 只能传递可序列化的值,`Function`、`BigInt` 或 `Symbol` 不起作用,`ArrayBuffer` 之类的东西也可能不行。
277
- 2. Vercel 服务器将头部大小(查询字符串和其他内容)限制为 **14KB**,因此请将 URL 状态保持在约 5000 字以内。 https://vercel.com/docs/errors/URL_TOO_LONG
357
+ 1. 只能传递可序列化的值,`Function`、`BigInt`或`Symbol`不会工作,可能类似`ArrayBuffer`的东西也不行。
358
+ 2. Vercel服务器限制头部(查询字符串和其他内容)的大小为**14KB**,所以保持您的URL状态在~5000个单词以下。https://vercel.com/docs/errors/URL_TOO_LONG
359
+ 3. 已在使用app router的`next.js` 14中测试,不计划支持pages。
278
360
 
279
361
  ## 本地运行
280
362
 
281
- 克隆此仓库,运行 `npm install` 然后
363
+ 克隆此仓库,运行`npm install`然后
282
364
 
283
365
  ```sh
284
366
  npm run dev
285
367
  ```
286
368
 
287
- 转到 [localhost:3000](http://localhost:3000)
369
+ 转到[localhost:3000](http://localhost:3000)
288
370
 
289
371
  ## 联系与支持
290
372
 
291
- - 创建 [GitHub issue](https://github.com/asmyshlyaev177/state-in-url/issues) 以报告错误、请求功能或提出问题
373
+ - 创建[GitHub issue](https://github.com/asmyshlyaev177/state-in-url/issues)报告bug、提出功能请求或提问
292
374
 
293
375
  ## [更新日志](https://github.com/asmyshlyaev177/state-in-url/blob/main/CHANGELOG.md)
294
376
 
295
377
  ## 许可证
296
378
 
297
- 本项目采用 [MIT 许可证](https://github.com/asmyshlyaev177/state-in-url/blob/main/LICENSE)。
379
+ 本项目采用[MIT许可证](https://github.com/asmyshlyaev177/state-in-url/blob/main/LICENSE)。
298
380
 
299
381
  ## 灵感来源
300
382
 
301
- [在 Vue 中使用 URL 存储状态](https://dev.to/jacobandrewsky/using-url-to-store-state-in-vue-275c)
383
+ [在Vue中使用URL存储状态](https://dev.to/jacobandrewsky/using-url-to-store-state-in-vue-275c)
302
384
 
303
- [在 URL 中存储状态](https://antonz.org/storing-state/)
385
+ [在URL中存储状态](https://antonz.org/storing-state/)
304
386
 
305
387
  [NextJS useSearchParams](https://nextjs.org/docs/app/api-reference/functions/use-search-params)
package/README.md CHANGED
@@ -98,7 +98,7 @@ In `tsconfig.json` in `compilerOptions` set `"moduleResolution": "Node16"` or `"
98
98
 
99
99
  ## useUrlState hook for Next.js
100
100
 
101
- [Docs](packages/urlstate/useUrlState/README.md)
101
+ [Docs](ackages/urlstate/next/useUrlState#api)
102
102
 
103
103
  `useUrlState` is a custom React hook for Next.js applications that make communication between client components easy. It allows you to share any complex state and sync it with the URL search parameters, providing a way to persist state across page reloads and share application state via URLs.
104
104
 
@@ -120,7 +120,7 @@ In `tsconfig.json` in `compilerOptions` set `"moduleResolution": "Node16"` or `"
120
120
 
121
121
  ```typescript
122
122
  'use client'
123
- import { useUrlState } from 'state-in-url';
123
+ import { useUrlState } from 'state-in-url/next';
124
124
 
125
125
  import { countState } from './countState';
126
126
 
@@ -176,7 +176,7 @@ export const userSettings: UserSettings {
176
176
 
177
177
  ```typescript
178
178
  'use client'
179
- import { useUrlState } from 'state-in-url';
179
+ import { useUrlState } from 'state-in-url/next';
180
180
 
181
181
  import { userSettings } from './userSettings';
182
182
 
@@ -246,11 +246,78 @@ function Component() {
246
246
  }, [state, updateUrl]);
247
247
  ```
248
248
 
249
+ #### With server side rendering
250
+
251
+ ```typescript
252
+ export default async function Home({ searchParams }: { searchParams: object }) {
253
+ return (
254
+ <Form sp={searchParams} />
255
+ )
256
+ }
257
+
258
+ // Form.tsx
259
+ 'use client'
260
+ import React from 'react';
261
+ import { useUrlState } from 'state-in-url/next';
262
+ import { form } from './form';
263
+
264
+ const Form = ({ sp }: { sp: object }) => {
265
+ const { state, updateState, updateUrl } = useUrlState(form, sp);
266
+ }
267
+ ```
268
+
269
+ #### Using hook in `layout` component
270
+
271
+ That a tricky part, since nextjs with app router doesn't allow to access searchParams from server side. There is workaround with using middleware, but it isn't pretty and can stop working after nextjs update.
272
+
273
+ ```typescript
274
+ // add to appropriate `layout.tsc`
275
+ export const runtime = 'edge';
276
+
277
+ // middleware.ts
278
+ import type { NextRequest } from 'next/server';
279
+ import { NextResponse } from 'next/server';
280
+
281
+ export function middleware(request: NextRequest) {
282
+ const url = request.url?.includes('_next') ? null : request.url;
283
+ const sp = url?.split?.('?')?.[1] || '';
284
+
285
+ const response = NextResponse.next();
286
+
287
+ if (url !== null) {
288
+ response.headers.set('searchParams', sp);
289
+ }
290
+
291
+ return response;
292
+ }
293
+
294
+ // Target layout component
295
+ import { headers } from 'next/headers';
296
+ import { decodeState } from 'state-in-url/encodeState';
297
+
298
+ export default async function Layout({
299
+ children,
300
+ }: {
301
+ children: React.ReactNode;
302
+ }) {
303
+ const sp = headers().get('searchParams') || '';
304
+
305
+ return (
306
+ <div>
307
+ <Comp1 searchParams={decodeState(sp, stateShape)} />
308
+ {children}
309
+ </div>
310
+ );
311
+ }
312
+
313
+
314
+ ```
315
+
249
316
  #### With arbitrary state shape (not recommended)
250
317
 
251
318
  ```typescript
252
319
  'use client'
253
- import { useUrlState } from 'state-in-url';
320
+ import { useUrlState } from 'state-in-url/next';
254
321
 
255
322
  const someObj = {};
256
323
 
@@ -293,12 +360,14 @@ function SettingsComponent() {
293
360
  - Use TypeScript for enhanced type safety and autocomplete
294
361
  - Avoid storing sensitive information in URL parameters
295
362
  - Use `updateState` for frequent updates and `updateUrl` to sync changes to url
363
+ - Use `Suspence` to wrap client components in Next.js
296
364
  - Use this [extension](https://marketplace.visualstudio.com/items?itemName=yoavbls.pretty-ts-errors) for readable TS errors
297
365
 
298
366
  ## Gothas
299
367
 
300
368
  1. Can pass only serializable values, `Function`, `BigInt` or `Symbol` won't work, probably things like `ArrayBuffer` neither.
301
369
  2. Vercel servers limit size of headers (query string and other stuff) to **14KB**, so keep your URL state under ~5000 words. https://vercel.com/docs/errors/URL_TOO_LONG
370
+ 3. Tested with `next.js` 14 with app router, no plans to support pages.
302
371
 
303
372
  ## Run locally
304
373
 
@@ -0,0 +1 @@
1
+ const n={string:"◖",boolean:"🗵",null:"∙null",undefined:"∙undefined",number:"∓",date:"⏲"};export{n as SYMBOLS};
@@ -0,0 +1 @@
1
+ import{encode as t,decode as r}from"../encoder/encoder.mjs";import{getParams as n}from"../utils.mjs";function e(r,e,o){const i=n(o);return Object.entries(r||{}).forEach((([r,n])=>{const o=e?.[r];JSON.stringify(n)!==JSON.stringify(o)&&i.set(r,t(n))})),i.toString()}function o(t,e){return{...e||{},...Object.fromEntries([...n(t).entries()].map((([t,n])=>{const o=e?.[t];return[t,r(n,o)??o]})))}}export{o as decodeState,e as encodeState};
@@ -33,4 +33,11 @@ export declare const reviver: (key: string, value: unknown) => unknown;
33
33
  * * Github {@link https://github.com/asmyshlyaev177/state-in-url}
34
34
  */
35
35
  export declare function parseJSON<T extends JSONCompatible>(jsonString: string, fallbackValue?: T): T | Primitive | undefined;
36
+ /**
37
+ * Parses the Next.js server-side `searchParams` into an object.
38
+ *
39
+ * @param {object | undefined} sp - The server-side query string to parse.
40
+ * @param {T} defaults - The default values to use if parsing fails.
41
+ * @return {T} - The parsed object or the default values.
42
+ */
36
43
  export declare function parseSsrQs<T extends JSONCompatible>(sp: object | undefined, defaults: T): T;
@@ -0,0 +1 @@
1
+ import{SYMBOLS as t}from"../constants/constants.mjs";import{typeOf as r}from"../utils.mjs";function n(n){switch(r(n)){case"function":case"symbol":return"";case"date":return t.date+n.toISOString();case"string":return`${t.string}${encodeURIComponent(n)}`;case"number":return t.number+String(n);case"boolean":return t.boolean+String(n);case"object":case"array":return JSON.stringify(n,s).replaceAll('"',"'");case"null":return t.null;case"undefined":return t.undefined;default:return String(n)}}function e(t,r){return a(t.replaceAll("'",'"'),r)}const s=(t,e)=>{const s=r(e);return t&&"object"!==s&&"array"!==s?n(e):e},o=r=>r===t.null?null:r!==t.undefined?r?.startsWith?.(t.number)?Number.parseFloat(r.replace(t.number,"")):r?.startsWith?.(t.boolean)?!!r.includes("true"):r?.startsWith?.(t.date)?new Date(r.slice(1)):r?.startsWith?.(t.string)?decodeURIComponent(r).replace(/^◖/,""):c:void 0,c=Symbol("isError"),u=(t,r)=>{const n="string"==typeof r,e=n&&o(r);return e===c?r:t&&n?e:r};function a(t,r){try{return JSON.parse(t,u)}catch{const n=o(t);return n!==c?n:r}}function i(t,r){return{...r,...f(JSON.stringify(t),r)}}const l=(t,r)=>{const n="string"==typeof r,s=n&&e(r?.replaceAll?.("'",'"'));return t&&n?s:r};function f(t,r){try{return JSON.parse(t,l)}catch{const n=o(t);return n!==c?n:r}}export{e as decode,o as decodePrimitive,n as encode,c as errorSym,a as parseJSON,i as parseSsrQs,s as replacer,u as reviver};
package/dist/index.d.ts CHANGED
@@ -1 +1,9 @@
1
- export { encodeState, decodeState, encode, decode, type Type, useUrlEncode, useUrlState, } from './urlstate';
1
+ import { decode, encode, parseSsrQs } from './encoder';
2
+ import { decodeState, encodeState } from './encodeState';
3
+ import { useUrlState } from './next/useUrlState';
4
+ import { useSharedState } from './useSharedState';
5
+ import { useUrlEncode } from './useUrlEncode';
6
+ import { useUrlStateBase } from './useUrlStateBase';
7
+ import { isSSR, type JSONCompatible, type Type, typeOf } from './utils';
8
+ export { decode, decodeState, encode, encodeState, isSSR, parseSsrQs, typeOf, useSharedState, useUrlEncode, useUrlState, useUrlStateBase, };
9
+ export type { JSONCompatible, Type };
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?s(t):t?.toString?.()||""),s=t=>t.split("?")?.[1]||t||"",a=(t,e)=>JSON.stringify(t)===JSON.stringify(e);function u(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,l).replaceAll('"',"'");case"null":return n.null;case"undefined":return n.undefined;default:return String(t)}}function i(t,e){return function(t,e){try{return JSON.parse(t,g)}catch{const n=f(t);return n!==d?n:e}}(t.replaceAll("'",'"'),e)}const l=(t,e)=>{const n=r(e);return t&&"object"!==n&&"array"!==n?u(e):e},f=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(/^◖/,""):d:void 0,d=Symbol("isError"),g=(t,e)=>{const n="string"==typeof e,r=n&&f(e);return r===d?e:t&&n?r:e};const p=(t,e)=>{const n="string"==typeof e,r=n&&i(e?.replaceAll?.("'",'"'));return t&&n?r:e};function y(t,e){try{return JSON.parse(t,p)}catch{const n=f(t);return n!==d?n:e}}function b(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,u(n))})),r.toString()}function h(t,e){return{...e||{},...Object.fromEntries([...o(t).entries()].map((([t,n])=>{const r=e?.[t];return[t,i(n,r)??r]})))}}const S=new WeakMap,m=new WeakMap,w={get:t=>m.get(t)||[],add(t,e){const n=this.get(t);return m.set(t,n.concat(e)),()=>this.remove(t,e)},remove(t,e){const n=this.get(t).filter((t=>t!==e));n.length?m.set(t,n):m.delete(t)}},O={get:function(t){return S.get(t)},set:function(t,e){S.set(t,e)}},E=t.useInsertionEffect||t.useEffect;function N(e){const n=t.useCallback((function(t,n){return"object"===r(t)?b(t,e,n):""}),[e]);return{parse:t.useCallback((function(t){return h(t,e)}),[e]),stringify:n}}function v(n,r){const{parse:o,stringify:s}=N(n),{state:u,getState:i,setState:l}=function(e,n){const r=t.useRef(e),[o,s]=t.useState((()=>{if(c())return n?n?.():r.current;const t=n?.()||O.get(r.current)||r.current,e=O.get(r.current);return a(e,t)||O.set(r.current,t),t})),u=t.useCallback((t=>{const e=O.get(r.current);if("function"==typeof t){const n=t(e);if(a(e,n))return;O.set(r.current,n),w.get(r.current).forEach((t=>{t()}))}else{if(a(e,t))return;O.set(r.current,t),w.get(r.current).forEach((t=>{t()}))}}),[]);return E((()=>{const t=w.add(r.current,(()=>{s(O.get(r.current)||r.current)}));return()=>{t()}}),[]),{state:o,getState:t.useCallback((()=>O.get(r.current)||r.current),[]),setState:u}}(n,(()=>{return c()?(t=r,e=n,{...e,...y(JSON.stringify(t),e)}):o(window.location.search);var t,e})),f=e();t.useInsertionEffect((()=>{const t=()=>{const t=o(window.location.search);l(t)},e="popstate";return window.addEventListener(e,t),()=>{window.removeEventListener(e,t)}}),[l]);const 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(i()),o=s(c)):(c=t??i(),o=s(c)),l(c);const a=`${window.location.pathname}${o.length?"?":""}${o}`;if(r!==a){const{replace:t,...n}=e||{};f[t?"replace":"push"](a,{scroll:!1,...n})}}),[f,s,i]);return{updateState:l,updateUrl:d,state:u}}export{i as decode,h as decodeState,u as encode,b as encodeState,N as useUrlEncode,v as useUrlState};
1
+ export{decode,encode,parseSsrQs}from"./encoder/encoder.mjs";export{decodeState,encodeState}from"./encodeState/encodeState.mjs";export{useUrlState}from"./next/useUrlState/useUrlState.mjs";export{useSharedState}from"./useSharedState/useSharedState.mjs";export{useUrlEncode}from"./useUrlEncode/useUrlEncode.mjs";export{useUrlStateBase}from"./useUrlStateBase/useUrlStateBase.mjs";export{isSSR,typeOf}from"./utils.mjs";
@@ -1,5 +1,5 @@
1
1
  import { useRouter } from 'next/navigation';
2
- import { type DeepReadonly, type JSONCompatible } from '../utils';
2
+ import { type DeepReadonly, type JSONCompatible } from '../../utils';
3
3
  /**
4
4
  * NextJS hook. Returns `state`, `updateState`, and `updateUrl` functions
5
5
  *
@@ -10,13 +10,15 @@ import { type DeepReadonly, type JSONCompatible } from '../utils';
10
10
  * ```ts
11
11
  * export const form = { name: '' };
12
12
  * const { state, updateState, updateUrl } = useUrlState(form);
13
+ * // for nextjs seerver components
14
+ * // const { state, updateState, updateUrl } = useUrlState(form, searchParams);
13
15
  *
14
16
  * updateState({ name: 'test' });
15
17
  * // by default it's uses router.push with scroll: false
16
18
  * updateUrl({ name: 'test' }, { replace: true, scroll: true });
17
19
  * ```
18
20
  *
19
- * * Github {@link https://github.com/asmyshlyaev177/state-in-url/tree/main/packages/urlstate/useUrlState#api}
21
+ * * Github {@link https://github.com/asmyshlyaev177/state-in-url/tree/main/packages/urlstate/next/useUrlState#api}
20
22
  */
21
23
  export declare function useUrlState<T extends JSONCompatible>(defaultState: T, searchParams?: object): {
22
24
  /**
@@ -27,7 +29,7 @@ export declare function useUrlState<T extends JSONCompatible>(defaultState: T, s
27
29
  * updateState(curr => ({ ...curr, name: 'test' }) );
28
30
  * ```
29
31
  *
30
- * * Github {@link https://github.com/asmyshlyaev177/state-in-url/tree/main/packages/urlstate/useUrlState#updatestate}
32
+ * * Github {@link https://github.com/asmyshlyaev177/state-in-url/tree/main/packages/urlstate/next/useUrlState#updatestate}
31
33
  */
32
34
  updateState: (value: T | DeepReadonly<T> | ((currState: T) => T)) => void;
33
35
  /**
@@ -38,13 +40,14 @@ export declare function useUrlState<T extends JSONCompatible>(defaultState: T, s
38
40
  * updateUrl(curr => ({ ...curr, name: 'test' }), { replace: true, scroll: false } );
39
41
  * ```
40
42
  *
41
- * * Github {@link https://github.com/asmyshlyaev177/state-in-url/tree/main/packages/urlstate/useUrlState#updateurl}
43
+ * * Github {@link https://github.com/asmyshlyaev177/state-in-url/tree/main/packages/urlstate/next/useUrlState#updateurl}
42
44
  */
43
45
  updateUrl: (value?: T | DeepReadonly<T> | ((currState: T) => T) | undefined, options?: Options) => void;
44
- state: DeepReadonly<T>;
46
+ state: DeepReadonly<DeepReadonly<T>>;
47
+ getState: () => T;
45
48
  };
46
49
  type Router = ReturnType<typeof useRouter>;
47
- type RouterOptions = NonNullable<Parameters<Router['push']>[1]>;
50
+ type RouterOptions = NonNullable<Parameters<Router['push']>[1] | Parameters<Router['replace']>[1]>;
48
51
  interface Options extends RouterOptions {
49
52
  replace?: boolean;
50
53
  }
@@ -0,0 +1 @@
1
+ import{useRouter as t,useSearchParams as e}from"next/navigation";import r from"react";import{parseSsrQs as a}from"../../encoder/encoder.mjs";import{useUrlStateBase as o}from"../../useUrlStateBase/useUrlStateBase.mjs";function s(s,m){const n=t(),{state:u,updateState:c,updateUrl:i,getState:p}=o(s,n,m),f=r.useCallback(((t,e)=>{i(t,{scroll:!1,...e})}),[i]),l=e();return r.useEffect((()=>{c(a(Object.fromEntries([...l.entries()]),s))}),[l]),{updateState:c,updateUrl:f,state:u,getState:p}}export{s as useUrlState};
@@ -0,0 +1 @@
1
+ const e=new WeakMap,t=new WeakMap,n={get:e=>t.get(e)||[],add(e,n){const s=this.get(e);return t.set(e,s.concat(n)),()=>this.remove(e,n)},remove(e,n){const s=this.get(e).filter((e=>e!==n));s.length?t.set(e,s):t.delete(e)}},s={get:function(t){return e.get(t)},set:function(t,n){e.set(t,n)}};export{s as stateMap,n as subscribers};
@@ -0,0 +1 @@
1
+ import e from"react";const t=e.useInsertionEffect||e.useEffect;export{t as useInsertionEffect};
@@ -0,0 +1 @@
1
+ import t from"react";import{stateMap as r,subscribers as e}from"../subscribers.mjs";import{useInsertionEffect as n}from"../useInsertionEffect.mjs";import{isSSR as u,isEqual as c}from"../utils.mjs";function s(s,o){const f=t.useRef(s),[i,a]=t.useState((()=>{if(u())return o?o?.():f.current;const t=o?.()||r.get(f.current)||f.current,e=r.get(f.current);return c(e,t)||r.set(f.current,t),t})),m=t.useCallback((t=>{const n=r.get(f.current);if("function"==typeof t){const u=t(n);if(c(n,u))return;r.set(f.current,u),e.get(f.current).forEach((t=>{t()}))}else{if(c(n,t))return;r.set(f.current,t),e.get(f.current).forEach((t=>{t()}))}}),[]);n((()=>{const t=e.add(f.current,(()=>{a(r.get(f.current)||f.current)}));return()=>{t()}}),[]);return{state:i,getState:t.useCallback((()=>r.get(f.current)||f.current),[]),setState:m}}export{s as useSharedState};
@@ -0,0 +1 @@
1
+ import t from"react";import{encodeState as r,decodeState as e}from"../encodeState/encodeState.mjs";import{typeOf as o}from"../utils.mjs";function n(n){const c=t.useCallback((function(t,e){return"object"===o(t)?r(t,n,e):""}),[n]);return{parse:t.useCallback((function(t){return e(t,n)}),[n]),stringify:c}}export{n as useUrlEncode};
@@ -0,0 +1 @@
1
+ export * from './useUrlStateBase';
@@ -0,0 +1,31 @@
1
+ import { type DeepReadonly, type JSONCompatible } from '../utils';
2
+ type Router = {
3
+ push: (href: string, opts: object) => void;
4
+ replace: (href: string, opts: object) => void;
5
+ };
6
+ /**
7
+ * Base hook to integrate it with different routers
8
+ *
9
+ * @param {JSONCompatible<T>} [defaultState] Fallback (default) values for state
10
+ * @param {Router} [router] Router object with `push` and `replace` methods
11
+ * @param {?SearchParams<T>} [searchParams] searchParams from Next server component
12
+ *
13
+ * * Example:
14
+ * ```ts
15
+ * export const form = { name: '' };
16
+ * const router = { push: () => {}, replace: () => {} };
17
+ * const { state, updateState, updateUrl } = useUrlStateBase(form, router, sp);
18
+ * ```
19
+ *
20
+ * * Github {@link https://github.com/asmyshlyaev177/state-in-url/tree/integrations/packages/urlstate/useUrlStateBase#api}
21
+ */
22
+ export declare function useUrlStateBase<T extends JSONCompatible>(defaultState: T, router: Router, searchParams?: object): {
23
+ updateState: (value: T | DeepReadonly<T> | ((currState: T) => T)) => void;
24
+ updateUrl: (value?: T | DeepReadonly<T> | ((currState: T) => T) | undefined, options?: Options) => void;
25
+ state: DeepReadonly<T>;
26
+ getState: () => T;
27
+ };
28
+ interface Options extends Object {
29
+ replace?: boolean;
30
+ }
31
+ export {};
@@ -0,0 +1 @@
1
+ import t from"react";import{parseSsrQs as e}from"../encoder/encoder.mjs";import{useInsertionEffect as o}from"../useInsertionEffect.mjs";import{useSharedState as n}from"../useSharedState/useSharedState.mjs";import{useUrlEncode as r}from"../useUrlEncode/useUrlEncode.mjs";import{isSSR as s}from"../utils.mjs";function a(a,i,m){const{parse:d,stringify:p}=r(a),{state:l,getState:u,setState:w}=n(a,(()=>s()?e(m,a):d(window.location.search)));o((()=>{const t=()=>{const t=d(window.location.search);w(t)};return window.addEventListener(c,t),()=>{window.removeEventListener(c,t)}}),[w]);const f=t.useCallback(((t,e)=>{const o=window.location.search,n=`${window.location.pathname}${o.length&&!o.includes("?")?"?":""}${o}`;let r,s;"function"==typeof t?(r=t(u()),s=p(r)):(r=t??u(),s=p(r)),w(r);const a=`${window.location.pathname}${s.length?"?":""}${s}`;if(n!==a){const{replace:t,...o}=e||{};i[t?"replace":"push"](a,{...o})}}),[i,p,u]);return{updateState:w,updateUrl:f,state:l,getState:u}}const c="popstate";export{a as useUrlStateBase};
package/dist/utils.mjs ADDED
@@ -0,0 +1 @@
1
+ const t=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},e=()=>"undefined"==typeof window,n=t=>new URLSearchParams("string"==typeof t?r(t):t?.toString?.()||""),r=t=>t.split("?")?.[1]||t||"",o=(t,e)=>JSON.stringify(t)===JSON.stringify(e);export{n as getParams,o as isEqual,e as isSSR,t as typeOf};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "state-in-url",
3
- "version": "1.2.0",
3
+ "version": "1.3.0-beta.2",
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": [
@@ -42,27 +42,35 @@
42
42
  "files": [
43
43
  "dist"
44
44
  ],
45
- "main": "dist/index.cjs",
45
+ "main": "dist/index.mjs",
46
46
  "exports": {
47
- "import": {
47
+ ".": {
48
48
  "types": "./dist/index.d.ts",
49
49
  "default": "./dist/index.mjs"
50
50
  },
51
- "node": {
52
- "types": "./dist/index.d.ts",
53
- "default": "./dist/index.mjs"
51
+ "./next": {
52
+ "types": "./dist/next/useUrlState/index.d.ts",
53
+ "default": "./dist/next/useUrlState/useUrlState.mjs"
54
54
  },
55
- "require": {
56
- "types": "./dist/index.d.ts",
57
- "default": "./dist/index.cjs"
55
+ "./encodeState": {
56
+ "types": "./dist/next/encodeState/index.d.ts",
57
+ "default": "./dist/next/encodeState/encodeState.mjs"
58
+ },
59
+ "./encoder": {
60
+ "types": "./dist/next/encoder/index.d.ts",
61
+ "default": "./dist/next/encoder/encoder.mjs"
58
62
  },
59
- "module": {
60
- "types": "./dist/index.mjs",
61
- "default": "./dist/index.d.ts"
63
+ "./useSharedState": {
64
+ "types": "./dist/next/useSharedState/index.d.ts",
65
+ "default": "./dist/next/useSharedState/useSharedState.mjs"
62
66
  },
63
- "default": {
64
- "types": "./dist/index.mjs",
65
- "default": "./dist/index.d.ts"
67
+ "./useUrlEncode": {
68
+ "types": "./dist/next/useUrlEncode/index.d.ts",
69
+ "default": "./dist/next/useUrlEncode/useUrlEncode.mjs"
70
+ },
71
+ "./useUrlStateBase": {
72
+ "types": "./dist/next/useUrlStateBase/index.d.ts",
73
+ "default": "./dist/next/useUrlStateBase/useUrlStateBase.mjs"
66
74
  }
67
75
  },
68
76
  "types": "./dist/index.d.ts",
@@ -90,8 +98,7 @@
90
98
  "test": {
91
99
  "dependencies": [
92
100
  "tsc",
93
- "test:unit",
94
- "kill-next"
101
+ "test:unit"
95
102
  ],
96
103
  "command": "npm run test:int -- --retries=1"
97
104
  },
@@ -115,7 +122,8 @@
115
122
  },
116
123
  "dev": {
117
124
  "dependencies": [
118
- "dev:lib"
125
+ "dev:lib",
126
+ "kill-next"
119
127
  ],
120
128
  "service": true,
121
129
  "command": "npm run dev:nextjs"
package/dist/index.cjs DELETED
@@ -1 +0,0 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e=require("react"),t=require("next/navigation");function n(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var r=n(e);const o={string:"◖",boolean:"🗵",null:"∙null",undefined:"∙undefined",number:"∓",date:"⏲"},s=e=>{const t=null===e,n=Array.isArray(e),r=e instanceof Date;return(t?"null":r&&"date")||n&&"array"||!t&&!r&&!n&&"object"==typeof e&&"object"||typeof e},c=()=>"undefined"==typeof window,u=e=>new URLSearchParams("string"==typeof e?a(e):e?.toString?.()||""),a=e=>e.split("?")?.[1]||e||"",i=(e,t)=>JSON.stringify(e)===JSON.stringify(t);function l(e){switch(s(e)){case"function":case"symbol":return"";case"date":return o.date+e.toISOString();case"string":return`${o.string}${encodeURIComponent(e)}`;case"number":return o.number+String(e);case"boolean":return o.boolean+String(e);case"object":case"array":return JSON.stringify(e,d).replaceAll('"',"'");case"null":return o.null;case"undefined":return o.undefined;default:return String(e)}}function f(e,t){return function(e,t){try{return JSON.parse(e,y)}catch{const n=p(e);return n!==g?n:t}}(e.replaceAll("'",'"'),t)}const d=(e,t)=>{const n=s(t);return e&&"object"!==n&&"array"!==n?l(t):t},p=e=>e===o.null?null:e!==o.undefined?e?.startsWith?.(o.number)?Number.parseFloat(e.replace(o.number,"")):e?.startsWith?.(o.boolean)?!!e.includes("true"):e?.startsWith?.(o.date)?new Date(e.slice(1)):e?.startsWith?.(o.string)?decodeURIComponent(e).replace(/^◖/,""):g:void 0,g=Symbol("isError"),y=(e,t)=>{const n="string"==typeof t,r=n&&p(t);return r===g?t:e&&n?r:t};const b=(e,t)=>{const n="string"==typeof t,r=n&&f(t?.replaceAll?.("'",'"'));return e&&n?r:t};function S(e,t){try{return JSON.parse(e,b)}catch{const n=p(e);return n!==g?n:t}}function h(e,t,n){const r=u(n);return Object.entries(e||{}).forEach((([e,n])=>{const o=t?.[e];JSON.stringify(n)!==JSON.stringify(o)&&r.set(e,l(n))})),r.toString()}function w(e,t){return{...t||{},...Object.fromEntries([...u(e).entries()].map((([e,n])=>{const r=t?.[e];return[e,f(n,r)??r]})))}}const m=new WeakMap,O=new WeakMap,v={get:e=>O.get(e)||[],add(e,t){const n=this.get(e);return O.set(e,n.concat(t)),()=>this.remove(e,t)},remove(e,t){const n=this.get(e).filter((e=>e!==t));n.length?O.set(e,n):O.delete(e)}},E={get:function(e){return m.get(e)},set:function(e,t){m.set(e,t)}},j=r.default.useInsertionEffect||r.default.useEffect;function N(e){const t=r.default.useCallback((function(t,n){return"object"===s(t)?h(t,e,n):""}),[e]);return{parse:r.default.useCallback((function(t){return w(t,e)}),[e]),stringify:t}}exports.decode=f,exports.decodeState=w,exports.encode=l,exports.encodeState=h,exports.useUrlEncode=N,exports.useUrlState=function(e,n){const{parse:o,stringify:s}=N(e),{state:u,getState:a,setState:l}=function(e,t){const n=r.default.useRef(e),[o,s]=r.default.useState((()=>{if(c())return t?t?.():n.current;const e=t?.()||E.get(n.current)||n.current,r=E.get(n.current);return i(r,e)||E.set(n.current,e),e})),u=r.default.useCallback((e=>{const t=E.get(n.current);if("function"==typeof e){const r=e(t);if(i(t,r))return;E.set(n.current,r),v.get(n.current).forEach((e=>{e()}))}else{if(i(t,e))return;E.set(n.current,e),v.get(n.current).forEach((e=>{e()}))}}),[]);return j((()=>{const e=v.add(n.current,(()=>{s(E.get(n.current)||n.current)}));return()=>{e()}}),[]),{state:o,getState:r.default.useCallback((()=>E.get(n.current)||n.current),[]),setState:u}}(e,(()=>{return c()?(t=n,r=e,{...r,...S(JSON.stringify(t),r)}):o(window.location.search);var t,r})),f=t.useRouter();r.default.useInsertionEffect((()=>{const e=()=>{const e=o(window.location.search);l(e)},t="popstate";return window.addEventListener(t,e),()=>{window.removeEventListener(t,e)}}),[l]);const d=r.default.useCallback(((e,t)=>{const n=window.location.search,r=`${window.location.pathname}${n.length&&!n.includes("?")?"?":""}${n}`;let o,c;"function"==typeof e?(o=e(a()),c=s(o)):(o=e??a(),c=s(o)),l(o);const u=`${window.location.pathname}${c.length?"?":""}${c}`;if(r!==u){const{replace:e,...n}=t||{};f[e?"replace":"push"](u,{scroll:!1,...n})}}),[f,s,a]);return{updateState:l,updateUrl:d,state:u}};
@@ -1,8 +0,0 @@
1
- import { decode, encode, parseSsrQs } from './encoder';
2
- import { decodeState, encodeState } from './encodeState';
3
- import { useSharedState } from './useSharedState';
4
- import { useUrlEncode } from './useUrlEncode';
5
- import { useUrlState } from './useUrlState';
6
- import { isSSR, type JSONCompatible, type Type, typeOf } from './utils';
7
- export { decode, decodeState, encode, encodeState, isSSR, parseSsrQs, typeOf, useSharedState, useUrlEncode, useUrlState, };
8
- export type { JSONCompatible, Type };
File without changes
File without changes
File without changes