state-in-url 4.0.9 → 4.1.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 +1 -1
- package/README.KO.md +1 -1
- package/README.md +104 -56
- package/dist/encoder/encoder.d.ts +8 -8
- package/dist/encoder/encoder.mjs +1 -1
- package/dist/next/useUrlState/useUrlState.d.ts +16 -4
- package/dist/next/useUrlState/useUrlState.mjs +1 -1
- package/dist/parseSPObj.mjs +1 -1
- package/dist/react-router/useUrlState/useUrlState.d.ts +14 -2
- package/dist/react-router/useUrlState/useUrlState.mjs +1 -1
- package/dist/testUtils.d.ts +1 -0
- package/dist/useUrlEncode/useUrlEncode.d.ts +1 -1
- package/dist/useUrlStateBase/useUrlStateBase.d.ts +16 -3
- package/dist/useUrlStateBase/useUrlStateBase.mjs +1 -1
- package/dist/utils.d.ts +3 -0
- package/dist/utils.mjs +1 -1
- package/package.json +4 -4
package/README.CN.md
CHANGED
|
@@ -89,7 +89,7 @@ pnpm add state-in-url
|
|
|
89
89
|
|
|
90
90
|
## Next.js的useUrlState钩子
|
|
91
91
|
|
|
92
|
-
[文档](packages/urlstate/next/useUrlState
|
|
92
|
+
[文档](packages/urlstate/next/useUrlState)
|
|
93
93
|
|
|
94
94
|
`useUrlState`是一个为Next.js应用程序设计的自定义React钩子,使客户端组件之间的通信变得简单。它允许你共享任何复杂的状态并将其与URL搜索参数同步,提供了一种在页面重新加载之间持久化状态并通过URL共享应用程序状态的方法。
|
|
95
95
|
|
package/README.KO.md
CHANGED
|
@@ -96,7 +96,7 @@ pnpm add state-in-url
|
|
|
96
96
|
|
|
97
97
|
## Next.js용 useUrlState 훅
|
|
98
98
|
|
|
99
|
-
[문서](packages/urlstate/next/useUrlState
|
|
99
|
+
[문서](packages/urlstate/next/useUrlState)
|
|
100
100
|
|
|
101
101
|
`useUrlState`는 Next.js 애플리케이션을 위한 사용자 정의 React 훅으로, 클라이언트 컴포넌트 간의 통신을 쉽게 만듭니다. 복잡한 상태를 공유하고 URL 검색 매개변수와 동기화할 수 있게 해주어, 페이지 새로고침 시 상태를 유지하고 URL을 통해 애플리케이션 상태를 공유할 수 있습니다.
|
|
102
102
|
|
package/README.md
CHANGED
|
@@ -1,29 +1,25 @@
|
|
|
1
1
|
English | [한국어](./README.KO.md) | [简体中文](./README.CN.md)
|
|
2
2
|
|
|
3
3
|
<div align="center">
|
|
4
|
-
<img src="/
|
|
5
|
-
|
|
6
|
-
<div><h2> Store any complex state in query parameters, URL synchronization (aka deep links) made easy. Share state between any unrelated components. With support of static types validation and server side rendering.</h2></div>
|
|
7
|
-
|
|
4
|
+
<img src="/packages/example-nextjs14/public/Logo_symbol.png" alt="state-in-url logo" width="120px" />
|
|
8
5
|
|
|
6
|
+
# State in url
|
|
9
7
|
</div>
|
|
10
8
|
|
|
11
|
-
<hr />
|
|
12
|
-
|
|
13
9
|
<div align="center">
|
|
14
|
-
<a href="https://github.com/asmyshlyaev177/state-in-url/blob/master/Limits.md" target="_blank">URI size limitation, <b>up to 12KB</b> is safe</a>
|
|
15
10
|
</div>
|
|
16
11
|
|
|
17
12
|
<div align="center">
|
|
18
|
-
<hr />
|
|
19
13
|
|
|
20
14
|
[](https://www.npmjs.com/package/state-in-url)
|
|
21
|
-

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

|
|
23
19
|
[](https://app.codacy.com/gh/asmyshlyaev177/state-in-url/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_coverage)
|
|
24
20
|
[](https://github.com/asmyshlyaev177/state-in-url/)
|
|
25
21
|
[]([https://github.com/semantic-release/semantic-release](https://github.com/asmyshlyaev177/state-in-url))
|
|
26
|
-
|
|
22
|
+
|
|
27
23
|
[](https://scorecard.dev/viewer/?uri=github.com/asmyshlyaev177/state-in-url)
|
|
28
24
|
[](https://www.bestpractices.dev/projects/9679)
|
|
29
25
|
<!-- or by embedding this in your HTML:
|
|
@@ -40,6 +36,8 @@ English | [한국어](./README.KO.md) | [简体中文](./README.CN.md)
|
|
|
40
36
|
<a href="https://state-in-url.dev" target="_blank">DEMO</a> |
|
|
41
37
|
<a href="https://state-in-url.netlify.app/" target="_blank">DEMO2</a>
|
|
42
38
|
|
|
39
|
+
<a href="https://github.com/asmyshlyaev177/state-in-url/blob/master/Limits.md" target="_blank">URI size limitation, <b>up to 12KB</b> is safe</a>
|
|
40
|
+
|
|
43
41
|
<hr />
|
|
44
42
|
|
|
45
43
|
Add a ⭐️ and <a href="https://github.com/asmyshlyaev177" target="_blank">follow me</a> to support the project!
|
|
@@ -56,9 +54,9 @@ Share if it useful for you.
|
|
|
56
54
|
|
|
57
55
|
</div>
|
|
58
56
|
|
|
59
|
-
|
|
57
|
+
## Why use `state-in-url`?
|
|
60
58
|
|
|
61
|
-
Store any user state in query parameters; imagine JSON in a browser URL, while keeping types and structure of data, e.g. numbers will be decoded as numbers not strings,
|
|
59
|
+
Store any user state in query parameters; imagine JSON in a browser URL, while keeping types and structure of data, e.g. numbers will be decoded as numbers not strings, objects and arrays supported.
|
|
62
60
|
Dead simple, fast, and with static Typescript validation. Deep links, aka URL synchronization, made easy.
|
|
63
61
|
|
|
64
62
|
Contains `useUrlState` hook for Next.js and react-router, and helpers for anything else on JS.
|
|
@@ -69,27 +67,27 @@ This library does all mundane stuff for you.
|
|
|
69
67
|
|
|
70
68
|
This library is a good alternative for NUQS.
|
|
71
69
|
|
|
72
|
-
|
|
70
|
+
### Use cases
|
|
73
71
|
|
|
74
|
-
-
|
|
75
|
-
-
|
|
76
|
-
-
|
|
77
|
-
-
|
|
72
|
+
- Store unsaved user forms or page filters in URL
|
|
73
|
+
- Just sync data between unrelated client components without touching URI
|
|
74
|
+
- Shareable URLs with application state (Deep linking, URL state synchronization)
|
|
75
|
+
- Easy state persistence across page reloads
|
|
78
76
|
|
|
79
|
-
|
|
77
|
+
### Features
|
|
80
78
|
|
|
81
79
|
- 🧩 **Simple**: No providers, reducers, boilerplate or new concepts, API similar to `React.useState`
|
|
82
|
-
- 📘 **Typescript validation**: State is just an object, automatic static validation in IDE/tests according to Typescript definition
|
|
80
|
+
- 📘 **Typescript validation/autocomplete**: State is just an object, automatic static validation in IDE/tests according to Typescript definition
|
|
83
81
|
- ✨ **Complex data**: Nested objects, dates and arrays, works same as JSON, but in URL
|
|
84
|
-
- 🔵 **Server Side Rendering**: Can use it in Server Components, Next.js 14 and 15 are supported
|
|
85
82
|
- ☂ **Default values**: Giving you default values if parameter not in url
|
|
86
83
|
- ⌨ **Organized**: All possible values defined at start, protect you from getting non existing key
|
|
87
|
-
-
|
|
88
|
-
-
|
|
89
|
-
-
|
|
90
|
-
-
|
|
91
|
-
-
|
|
92
|
-
-
|
|
84
|
+
- **Fast**: Minimal rerenders, less than [1ms](https://github.com/asmyshlyaev177/state-in-url/blob/87c8c7c995c5cd7d9e7aa039c30bfe64b24abe4b/packages/urlstate/encoder/encoder.test.ts#L185) to encode and decode big object
|
|
85
|
+
- **Server Side Rendering**: Can use it in Server Components, Next.js 14 and 15 are supported
|
|
86
|
+
- **Lightweight**: Zero dependencies, library less than 2KB
|
|
87
|
+
- **DX**: Good developer experience, documentation, JSDoc comments, and examples
|
|
88
|
+
- **Framework Flexibility**: Hooks for `Next.js` and `react-router`, helpers to use it with other frameworks or pure JS
|
|
89
|
+
- **Well tested**: Unit tests and Playwright tests for Chrome/Firefox/Safari
|
|
90
|
+
- **Permissive license**: MIT
|
|
93
91
|
|
|
94
92
|
## Table of content
|
|
95
93
|
|
|
@@ -136,26 +134,26 @@ Possibly need to set `"module": "ES2022"`, or `"module": "ESNext"`
|
|
|
136
134
|
Main hook that takes initial state as parameter and returns state object, callback to update url, and callback to update only state.
|
|
137
135
|
All components that use the same `state` object are automatically synchronized.
|
|
138
136
|
|
|
139
|
-
|
|
137
|
+
### useUrlState hook for Next.js
|
|
140
138
|
|
|
141
|
-
[Full API Docs](packages/urlstate/next/useUrlState
|
|
139
|
+
[Full API Docs](packages/urlstate/next/useUrlState)
|
|
142
140
|
|
|
143
141
|
[React-Router example](#useurlstate-hook-for-react-router)
|
|
144
142
|
|
|
145
|
-
|
|
143
|
+
#### Usage examples
|
|
146
144
|
|
|
147
|
-
|
|
145
|
+
##### Basic
|
|
148
146
|
|
|
149
147
|
1. Define state shape with default values
|
|
150
148
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
149
|
+
```typescript
|
|
150
|
+
// userState.ts
|
|
151
|
+
// Only parameters with value different from default will go to the url.
|
|
152
|
+
export const userState: UserState = { name: '', age: 0 }
|
|
155
153
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
154
|
+
// use `Type` not `Interface`!
|
|
155
|
+
type UserState = { name: string, age: number }
|
|
156
|
+
```
|
|
159
157
|
|
|
160
158
|
2. Import it and use
|
|
161
159
|
|
|
@@ -168,10 +166,11 @@ import { userState } from './userState';
|
|
|
168
166
|
function MyComponent() {
|
|
169
167
|
// can pass `replace` arg, it's control will `setUrl` will use `rounter.push` or `router.replace`, default replace=true
|
|
170
168
|
// can pass `searchParams` from server components
|
|
171
|
-
const { urlState, setUrl, setState } = useUrlState(userState);
|
|
169
|
+
const { urlState, setUrl, setState, reset } = useUrlState(userState);
|
|
172
170
|
|
|
173
171
|
return (
|
|
174
172
|
<div>
|
|
173
|
+
// urlState.name will return default value from `userState` if url empty
|
|
175
174
|
<input value={urlState.name}
|
|
176
175
|
// same api as React.useState, e.g. setUrl(currVal => currVal + 1)
|
|
177
176
|
onChange={(ev) => setUrl({ name: ev.target.value }) }
|
|
@@ -186,7 +185,7 @@ function MyComponent() {
|
|
|
186
185
|
onBlur={() => setUrl()}
|
|
187
186
|
/>
|
|
188
187
|
|
|
189
|
-
<button onClick={
|
|
188
|
+
<button onClick={reset}>
|
|
190
189
|
Reset
|
|
191
190
|
</button>
|
|
192
191
|
|
|
@@ -195,7 +194,56 @@ function MyComponent() {
|
|
|
195
194
|
}
|
|
196
195
|
```
|
|
197
196
|
|
|
198
|
-
|
|
197
|
+
##### Custom hook to work with slice of state conveniently
|
|
198
|
+
<details>
|
|
199
|
+
<Summary>Example</Summary>
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
'use client';
|
|
203
|
+
|
|
204
|
+
import React from 'react';
|
|
205
|
+
import { useUrlState } from 'state-in-url/next';
|
|
206
|
+
|
|
207
|
+
const form: Form = {
|
|
208
|
+
name: '',
|
|
209
|
+
age: undefined,
|
|
210
|
+
agree_to_terms: false,
|
|
211
|
+
tags: [],
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
type Form = {
|
|
215
|
+
name: string;
|
|
216
|
+
age?: number;
|
|
217
|
+
agree_to_terms: boolean;
|
|
218
|
+
tags: {id: string; value: {text: string; time: Date } }[];
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
export const useFormState = ({ searchParams }: { searchParams?: object }) => {
|
|
222
|
+
const { urlState, setUrl: setUrlBase, reset } = useUrlState(form, {
|
|
223
|
+
searchParams,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// first navigation will push new history entry
|
|
227
|
+
// all following will just replace that entry
|
|
228
|
+
// this way will have history with only 2 entries - ['/url', '/url?key=param']
|
|
229
|
+
|
|
230
|
+
const replace = React.useRef(false);
|
|
231
|
+
const setUrl = React.useCallback((
|
|
232
|
+
state: Parameters<typeof setUrlBase>[0],
|
|
233
|
+
opts?: Parameters<typeof setUrlBase>[1]
|
|
234
|
+
) => {
|
|
235
|
+
setUrlBase(state, { replace: replace.current, ...opts });
|
|
236
|
+
replace.current = true;
|
|
237
|
+
}, [setUrlBase]);
|
|
238
|
+
|
|
239
|
+
return { urlState, setUrl, resetUrl: reset };
|
|
240
|
+
};
|
|
241
|
+
```
|
|
242
|
+
</details>
|
|
243
|
+
|
|
244
|
+
<hr />
|
|
245
|
+
|
|
246
|
+
##### With complex state shape
|
|
199
247
|
|
|
200
248
|
<details>
|
|
201
249
|
<Summary>Example</Summary>
|
|
@@ -275,7 +323,7 @@ const tags = [
|
|
|
275
323
|
[Demo page example code](https://github.com/asmyshlyaev177/state-in-url/blob/master/packages/example-nextjs14/src/app/Form.tsx)
|
|
276
324
|
</details>
|
|
277
325
|
|
|
278
|
-
|
|
326
|
+
##### Update state only and sync to URL manually
|
|
279
327
|
|
|
280
328
|
<details>
|
|
281
329
|
<Summary>Example</Summary>
|
|
@@ -304,7 +352,7 @@ Syncing state `onBlur` will be more aligned with real world usage.
|
|
|
304
352
|
|
|
305
353
|
</details>
|
|
306
354
|
|
|
307
|
-
|
|
355
|
+
##### With server side rendering
|
|
308
356
|
|
|
309
357
|
<details>
|
|
310
358
|
<Summary>Example</Summary>
|
|
@@ -329,7 +377,7 @@ const Form = ({ searchParams }: { searchParams: object }) => {
|
|
|
329
377
|
|
|
330
378
|
</details>
|
|
331
379
|
|
|
332
|
-
|
|
380
|
+
##### Using hook in `layout` component
|
|
333
381
|
|
|
334
382
|
<details>
|
|
335
383
|
<Summary>Example</Summary>
|
|
@@ -380,7 +428,7 @@ export default async function Layout({
|
|
|
380
428
|
|
|
381
429
|
</details>
|
|
382
430
|
|
|
383
|
-
|
|
431
|
+
##### With arbitrary state shape (not recommended)
|
|
384
432
|
|
|
385
433
|
<details>
|
|
386
434
|
<Summary>Example</Summary>
|
|
@@ -398,13 +446,13 @@ function SettingsComponent() {
|
|
|
398
446
|
|
|
399
447
|
</details>
|
|
400
448
|
|
|
401
|
-
|
|
449
|
+
### useUrlState hook for React-Router
|
|
402
450
|
|
|
403
451
|
API is same as for Next.js version, except can pass options from [NavigateOptions](https://github.com/remix-run/react-router/blob/bc693ed9f39170bda13b9e1b282fb8e9d5925f66/packages/react-router/lib/context.ts#L99) type.
|
|
404
452
|
|
|
405
|
-
[API Docs](packages/urlstate/react-router/useUrlState
|
|
453
|
+
[API Docs](packages/urlstate/react-router/useUrlState)
|
|
406
454
|
|
|
407
|
-
|
|
455
|
+
#### Example
|
|
408
456
|
|
|
409
457
|
```typescript
|
|
410
458
|
export const form: Form = {
|
|
@@ -485,15 +533,15 @@ const tags = [
|
|
|
485
533
|
|
|
486
534
|
[Example code](packages/example-react-router6/src/Form-for-test.tsx)
|
|
487
535
|
|
|
488
|
-
|
|
536
|
+
## Other hooks and helpers
|
|
489
537
|
|
|
490
|
-
|
|
538
|
+
### `useUrlStateBase` hook for others routers
|
|
491
539
|
|
|
492
540
|
Hooks to create your own `useUrlState` hooks with other routers, e.g. react-router or tanstack router.
|
|
493
541
|
|
|
494
542
|
[API Docs](packages/urlstate/useUrlStateBase)
|
|
495
543
|
|
|
496
|
-
|
|
544
|
+
### `useSharedState` hook for React.js
|
|
497
545
|
|
|
498
546
|
Hook to share state between any React components, tested with Next.js and Vite.
|
|
499
547
|
|
|
@@ -510,19 +558,19 @@ function SettingsComponent() {
|
|
|
510
558
|
|
|
511
559
|
[API Docs](packages/urlstate/useSharedState/README.md)
|
|
512
560
|
|
|
513
|
-
|
|
561
|
+
### `useUrlEncode` hook for React.js
|
|
514
562
|
|
|
515
563
|
[API Docs](packages/urlstate/useUrlEncode/README.md)
|
|
516
564
|
|
|
517
|
-
|
|
565
|
+
### `encodeState` and `decodeState` helpers
|
|
518
566
|
|
|
519
567
|
[API Docs](packages/urlstate/encodeState/README.md)
|
|
520
568
|
|
|
521
|
-
|
|
569
|
+
### `encode` and `decode` helpers
|
|
522
570
|
|
|
523
571
|
[API Docs](packages/urlstate/encoder/README.md)
|
|
524
572
|
|
|
525
|
-
|
|
573
|
+
## Best Practices
|
|
526
574
|
|
|
527
575
|
- Define your state shape as a constant
|
|
528
576
|
- Use TypeScript for enhanced type safety and autocomplete
|
|
@@ -556,9 +604,9 @@ export const useUserState = () => {
|
|
|
556
604
|
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>
|
|
557
605
|
3. Tested with `next.js` 14/15 with app router, no plans to support pages.
|
|
558
606
|
|
|
559
|
-
|
|
607
|
+
## Other
|
|
560
608
|
|
|
561
|
-
|
|
609
|
+
### Contribute and/or run locally
|
|
562
610
|
|
|
563
611
|
See [Contributing doc](CONTRIBUTING.md)
|
|
564
612
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type JSONCompatible } from "../utils";
|
|
1
|
+
import { type JSONCompatible, type Simple } from "../utils";
|
|
2
2
|
/**
|
|
3
3
|
* Encode any JSON serializable value to URL friendly string
|
|
4
4
|
*
|
|
@@ -8,7 +8,7 @@ import { type JSONCompatible } from "../utils";
|
|
|
8
8
|
* * Docs {@link https://github.com/asmyshlyaev177/state-in-url/tree/master/packages/urlstate/encoder#encode}
|
|
9
9
|
*/
|
|
10
10
|
export declare function encode(payload: unknown): string;
|
|
11
|
-
export
|
|
11
|
+
export declare const encodePrimitive: (payload: Simple) => Simple;
|
|
12
12
|
/**
|
|
13
13
|
* Decode value part of URLSearchParams back to JS value
|
|
14
14
|
*
|
|
@@ -18,17 +18,17 @@ export type Primitive = Exclude<ReturnType<typeof decodePrimitive>, typeof error
|
|
|
18
18
|
*
|
|
19
19
|
* * Docs {@link https://github.com/asmyshlyaev177/state-in-url/tree/master/packages/urlstate/encoder#decode}
|
|
20
20
|
*/
|
|
21
|
-
export declare function decode<T>(payload: string, fallback?: T):
|
|
22
|
-
export declare const decodePrimitive: (str: string) => Date | typeof errorSym | undefined;
|
|
23
|
-
export declare const errorSym: unique symbol;
|
|
24
|
-
export declare const reviver: (_key: string, value: unknown) => unknown;
|
|
21
|
+
export declare function decode<T>(payload: string, fallback?: T): T;
|
|
25
22
|
/**
|
|
26
23
|
* Parses a JSON string into a TypeScript object.
|
|
27
24
|
*
|
|
28
25
|
* @param {string} jsonString - The JSON string to parse.
|
|
29
26
|
* @param {T} [fallbackValue] - The fallback value to use if parsing fails.
|
|
30
|
-
* @return {T |
|
|
27
|
+
* @return {T | CustomDecoded | undefined} - The parsed object or a primitive value, or undefined if parsing fails.
|
|
31
28
|
*
|
|
32
29
|
* * Docs {@link https://github.com/asmyshlyaev177/state-in-url}
|
|
33
30
|
*/
|
|
34
|
-
export declare function parseJSON<T extends JSONCompatible>(jsonString: string, fallbackValue?: T): T |
|
|
31
|
+
export declare function parseJSON<T extends JSONCompatible>(jsonString: string, fallbackValue?: T): T | CustomDecoded | undefined;
|
|
32
|
+
export declare function reviver(_key: string, value: unknown): unknown;
|
|
33
|
+
export declare const decodePrimitive: (str: string) => string | Date | undefined;
|
|
34
|
+
export type CustomDecoded = ReturnType<typeof decodePrimitive>;
|
package/dist/encoder/encoder.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{SYMBOLS as t}from"../constants/constants.mjs";import{typeOf as e}from"../utils.mjs";function n(n){if(
|
|
1
|
+
import{SYMBOLS as t}from"../constants/constants.mjs";import{typeOf as e}from"../utils.mjs";function n(t){return f(t)?t:JSON.stringify(t,r).replace(/'/g,"%27").replace(/"/g,"'")}function r(t,n){const a=e(n);if("object"!==a&&"array"!==a)return c(n);if("object"===a){const t=n;return Object.keys(t).forEach((e=>{t[e]=r("",t[e])})),t}return"array"===a?n.map((t=>r("",t))):n}const c=n=>{switch(e(n)){case"function":case"symbol":return"";case"date":return t.date+new Date(n).toISOString();case"undefined":return t.undefined;default:return n}};function a(t,e){return s(t.replace(/'/g,'"').replace(/%27/g,"'"),e)}function s(t,e){try{return JSON.parse(t,u)}catch{return e}}function u(t,e){return"string"==typeof e?i(e):e}const i=e=>{if(e!==t.undefined)return e?.startsWith?.(t.date)?new Date(e.slice(1)):e},o=new RegExp(`^(${t.undefined}|${t.date})`),f=t=>o.test(String(t));export{a as decode,i as decodePrimitive,n as encode,c as encodePrimitive,s as parseJSON,u as reviver};
|
|
@@ -19,10 +19,15 @@ export declare function useUrlState<T extends JSONCompatible>({ defaultState: T,
|
|
|
19
19
|
*
|
|
20
20
|
* @param {JSONCompatible<T>} [defaultState] Fallback (default) values for state
|
|
21
21
|
* @param {Object} params - Object with other parameters, including params from App router
|
|
22
|
-
* @param {boolean} params.replace replace URL
|
|
23
|
-
* @param {boolean} params.useHistory use window.history for navigation, default true
|
|
22
|
+
* @param {boolean} params.replace replace URL or push, default `true`
|
|
23
|
+
* @param {boolean} params.useHistory use window.history for navigation, default `true`, no _rsc requests https://github.com/vercel/next.js/discussions/59167
|
|
24
24
|
* @param {?SearchParams<T>} params.searchParams searchParams from Next server component
|
|
25
25
|
* @param {boolean} params.scroll reset scroll, default `false`
|
|
26
|
+
* @returns {Object} [result] State and callbacks
|
|
27
|
+
* @returns {Object} [result.state] - current state object
|
|
28
|
+
* @returns {Function} [result.setUrl] - function to update state and url
|
|
29
|
+
* @returns {Function} [result.setState] - function to update state only
|
|
30
|
+
* @returns {Function} [result.reset] - function to reset state and url to default
|
|
26
31
|
|
|
27
32
|
*
|
|
28
33
|
* * Example:
|
|
@@ -30,19 +35,26 @@ export declare function useUrlState<T extends JSONCompatible>({ defaultState: T,
|
|
|
30
35
|
* export const form = { name: '', age: 0 };
|
|
31
36
|
* const { urlState, setState, setUrl } = useUrlState(form);
|
|
32
37
|
* // for nextjs server components
|
|
33
|
-
* const { urlState, setState, setUrl } = useUrlState(form, { searchParams });
|
|
38
|
+
* const { urlState, setState, setUrl, reset } = useUrlState(form, { searchParams });
|
|
34
39
|
*
|
|
35
40
|
* setState({ name: 'test' });
|
|
36
41
|
* setUrl({ name: 'test' }, { replace: true, scroll: true });
|
|
37
42
|
* setUrl(curr => ({ ...curr, name: 'test' }), { replace: true, scroll: true });
|
|
43
|
+
* // reset state and url
|
|
44
|
+
* reset();
|
|
45
|
+
* reset({ replace: true });
|
|
46
|
+
* // same as setState(form) with setUrl(form)
|
|
38
47
|
* ```
|
|
39
48
|
*
|
|
40
|
-
* * Docs {@link https://github.com/asmyshlyaev177/state-in-url/tree/master/packages/urlstate/next/useUrlState
|
|
49
|
+
* * Docs {@link https://github.com/asmyshlyaev177/state-in-url/tree/master/packages/urlstate/next/useUrlState}
|
|
41
50
|
*/
|
|
42
51
|
export declare function useUrlState<T extends JSONCompatible>(defaultState: T, params?: Params): {
|
|
43
52
|
urlState: T;
|
|
44
53
|
setState: (value: Partial<T> | ((currState: T) => T)) => void;
|
|
45
54
|
setUrl: (value?: Partial<T> | ((currState: T) => T), options?: Options) => void;
|
|
55
|
+
reset: (options?: Options & {
|
|
56
|
+
[key: string]: unknown;
|
|
57
|
+
}) => void;
|
|
46
58
|
};
|
|
47
59
|
type Router = ReturnType<typeof useRouter>;
|
|
48
60
|
type RouterOptions = NonNullable<Parameters<Router["push"]>[1] | Parameters<Router["replace"]>[1]>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{useRouter as
|
|
1
|
+
import{useRouter as e,useSearchParams as t}from"next/navigation";import a from"react";import{parseSPObj as r}from"../../parseSPObj.mjs";import{useUrlStateBase as s}from"../../useUrlStateBase/useUrlStateBase.mjs";import{routerHistory as l,isSSR as o,filterUnknownParams as c,filterUnknownParamsClient as u}from"../../utils.mjs";function i(i,p){const n="defaultState"in i?i.defaultState:i,f="defaultState"in i?i.searchParams:p?.searchParams,S="defaultState"in i?i.useHistory:p?.useHistory,d="defaultState"in i?{scroll:i.scroll,replace:i.replace}:{scroll:p?.scroll,replace:p?.replace},j=void 0===S||S?l:e(),{state:U,updateState:b,updateUrl:g,reset:P,getState:h}=s(n,j,(({parse:e})=>o()?r(c(n,f),n):e(u(n)))),k=a.useMemo((()=>({...m,...d})),[]),v=a.useCallback(((e,t)=>g(e,{...k,...t})),[g]),x=t();a.useEffect((()=>{b(c(n,r(Object.fromEntries([...x.entries()]),n)))}),[x]);const y=a.useCallback((e=>{P({...k,...e})}),[P]);return{setState:b,updateState:b,setUrl:v,updateUrl:v,urlState:U,state:U,reset:y,getState:h}}const m={replace:!0,scroll:!1};export{i as useUrlState};
|
package/dist/parseSPObj.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{decode as r}from"./encoder/encoder.mjs";function t(r,t){return{...t,...n(JSON.stringify(r),t)}}function n(r,t){try{return JSON.parse(r,e)}catch{return t}}const e=(t,n)=>{const e="string"==typeof n,o=e&&r(n?.replace?.(/'/g,'"'));return t&&e?o:n};export{t as parseSPObj};
|
|
@@ -20,9 +20,14 @@ export declare function useUrlState<T extends JSONCompatible>({ defaultState: T,
|
|
|
20
20
|
* @param {JSONCompatible<T>} [defaultState] Fallback (default) values for state
|
|
21
21
|
* @param {Object} params - Object with other parameters
|
|
22
22
|
* @param {NavigateOptions} params.NavigateOptions See type from `react-router-dom`
|
|
23
|
-
* @param {boolean} params.replace replace URL
|
|
23
|
+
* @param {boolean} params.replace replace URL or push, default `true`
|
|
24
24
|
* @param {boolean} params.useHistory use window.history for navigation, default `false`
|
|
25
25
|
* @param {boolean} params.preventScrollReset keep scroll position, default `true`
|
|
26
|
+
* @returns {Object} [result] State and callbacks
|
|
27
|
+
* @returns {Object} [result.state] - current state object
|
|
28
|
+
* @returns {Function} [result.setUrl] - function to update state and url
|
|
29
|
+
* @returns {Function} [result.setState] - function to update state only
|
|
30
|
+
* @returns {Function} [result.reset] - function to reset state and url to default
|
|
26
31
|
* * Example:
|
|
27
32
|
* ```ts
|
|
28
33
|
* export const form = { name: '', age: 0 };
|
|
@@ -32,14 +37,21 @@ export declare function useUrlState<T extends JSONCompatible>({ defaultState: T,
|
|
|
32
37
|
* setUrl({ name: 'test' }, { replace: true });
|
|
33
38
|
* // similar to React.useState
|
|
34
39
|
* setUrl(curr => ({ ...curr, name: 'test' }), { replace: true });
|
|
40
|
+
* // reset state and url
|
|
41
|
+
* reset();
|
|
42
|
+
* reset({ replace: true });
|
|
43
|
+
* // same as setState(form) with setUrl(form)
|
|
35
44
|
* ```
|
|
36
45
|
*
|
|
37
|
-
* * Docs {@link https://github.com/asmyshlyaev177/state-in-url/tree/master/packages/urlstate/react-router/useUrlState
|
|
46
|
+
* * Docs {@link https://github.com/asmyshlyaev177/state-in-url/tree/master/packages/urlstate/react-router/useUrlState}
|
|
38
47
|
*/
|
|
39
48
|
export declare function useUrlState<T extends JSONCompatible>(defaultState: T, params?: Params): {
|
|
40
49
|
urlState: T;
|
|
41
50
|
setState: (value: Partial<T> | ((currState: T) => T)) => void;
|
|
42
51
|
setUrl: (value?: Partial<T> | ((currState: T) => T), options?: Params) => void;
|
|
52
|
+
reset: (options?: NavigateOptions & {
|
|
53
|
+
[key: string]: unknown;
|
|
54
|
+
}) => void;
|
|
43
55
|
};
|
|
44
56
|
type OldParams<T> = {
|
|
45
57
|
defaultState: T;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"react";import{useNavigate as t,useSearchParams as r}from"react-router-dom";import{parseSPObj as a}from"../../parseSPObj.mjs";import{useUrlStateBase as s}from"../../useUrlStateBase/useUrlStateBase.mjs";import{routerHistory as l,filterUnknownParamsClient as o,assignValue as p,filterUnknownParams as u}from"../../utils.mjs";function c(c,m){const n="defaultState"in c?c.defaultState:c,i="defaultState"in c?c.useHistory:m?.useHistory,f="defaultState"in c?{replace:c.replace,preventScrollReset:c.preventScrollReset}:{replace:m?.replace,preventScrollReset:m?.preventScrollReset},d=
|
|
1
|
+
import e from"react";import{useNavigate as t,useSearchParams as r}from"react-router-dom";import{parseSPObj as a}from"../../parseSPObj.mjs";import{useUrlStateBase as s}from"../../useUrlStateBase/useUrlStateBase.mjs";import{routerHistory as l,filterUnknownParamsClient as o,assignValue as p,filterUnknownParams as u}from"../../utils.mjs";function c(c,m){const n="defaultState"in c?c.defaultState:c,i="defaultState"in c?c.useHistory:m?.useHistory,f="defaultState"in c?{replace:c.replace,preventScrollReset:c.preventScrollReset}:{replace:m?.replace,preventScrollReset:m?.preventScrollReset},d=e.useMemo((()=>({...S,...f})),[]),j=t(),v=e.useMemo((()=>i?l:{replace:(e,t)=>j(e,{...d,...t}),push:(e,t)=>j(e,{...d,...t})}),[j]),{state:R,updateState:U,updateUrl:b,getState:g,reset:k}=s(n,v,(({parse:e})=>e(o(n)))),y=e.useCallback(((e,t)=>b(e,{...d,...t})),[b]),[B]=r();e.useEffect((()=>{U(p(n,u(n,a(Object.fromEntries([...B.entries()]),n))))}),[B]);const C=e.useCallback((e=>{k({...d,...e})}),[k]);return{setState:U,updateState:U,setUrl:y,updateUrl:y,urlState:R,state:R,reset:C,getState:g}}const S={replace:!0,preventScrollReset:!0};export{c as useUrlState};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const clone: (obj: object) => any;
|
|
@@ -17,7 +17,7 @@ import { type JSONCompatible } from "../utils";
|
|
|
17
17
|
* parse('name=Tom');
|
|
18
18
|
* ```
|
|
19
19
|
*
|
|
20
|
-
* * Docs {@link https://github.com/asmyshlyaev177/state-in-url/tree/master/packages/urlstate/useUrlEncode
|
|
20
|
+
* * Docs {@link https://github.com/asmyshlyaev177/state-in-url/tree/master/packages/urlstate/useUrlEncode}
|
|
21
21
|
*/
|
|
22
22
|
export declare function useUrlEncode<T extends JSONCompatible>(stateShape: T): {
|
|
23
23
|
/**
|
|
@@ -7,19 +7,31 @@ import { type JSONCompatible, type Router } from "../utils";
|
|
|
7
7
|
* @param {T} defaultState - An object representing the default state values.
|
|
8
8
|
* @param {Router} router - Router object with [push] and [replace] methods.
|
|
9
9
|
* @param {Function} [getInitialState] - Optional function to get the initial state, passes `parse`, and `filterUnknownParams` helpers
|
|
10
|
-
* @returns {Object}
|
|
10
|
+
* @returns {Object} [result] State and callbacks
|
|
11
|
+
* @returns {Object} [result.state] - current state object
|
|
12
|
+
* @returns {Function} [result.updateUrl] - function to update state and url
|
|
13
|
+
* @returns {Function} [result.updateState] - function to update state only
|
|
14
|
+
* @returns {Function} [result.reset] - function to reset state and url to default
|
|
11
15
|
*
|
|
12
16
|
*
|
|
13
17
|
* * Example:
|
|
14
18
|
* ```ts
|
|
15
19
|
* export const form = { name: '' };
|
|
16
20
|
* const router = { push: () => {}, replace: () => {} };
|
|
17
|
-
* const { state, updateState, updateUrl } = useUrlStateBase(form, router, ({ parse, filterClientSP }) =>
|
|
21
|
+
* const { state, updateState, updateUrl, reset } = useUrlStateBase(form, router, ({ parse, filterClientSP }) =>
|
|
18
22
|
* isSSR() ? getServerState() : getClientState()
|
|
19
23
|
* );
|
|
24
|
+
*
|
|
25
|
+
* updateState({ name: 'John' });
|
|
26
|
+
* updateState(curr => ({ ...curr, name: 'John' }));
|
|
27
|
+
* updateUrl({ name: 'John' }, { replace: true });
|
|
28
|
+
* updateUrl(curr => ({ ...curr, name: 'John' }), { replace: true });
|
|
29
|
+
* reset()
|
|
30
|
+
* reset({ replace: true })
|
|
31
|
+
*
|
|
20
32
|
* ```
|
|
21
33
|
*
|
|
22
|
-
* * Docs {@link https://github.com/asmyshlyaev177/state-in-url/tree/integrations/packages/urlstate/useUrlStateBase
|
|
34
|
+
* * Docs {@link https://github.com/asmyshlyaev177/state-in-url/tree/integrations/packages/urlstate/useUrlStateBase}
|
|
23
35
|
*/
|
|
24
36
|
export declare function useUrlStateBase<T extends JSONCompatible>(defaultState: T, router: Router, getInitialState?: ({ parse, }: {
|
|
25
37
|
parse: ReturnType<typeof useUrlEncode<T>>["parse"];
|
|
@@ -27,6 +39,7 @@ export declare function useUrlStateBase<T extends JSONCompatible>(defaultState:
|
|
|
27
39
|
updateState: (value: Partial<T> | ((currState: T) => T)) => void;
|
|
28
40
|
updateUrl: (value?: Parameters<(value: Partial<T> | ((currState: T) => T)) => void>[0], options?: Options) => void;
|
|
29
41
|
state: T;
|
|
42
|
+
reset: (options?: Options) => void;
|
|
30
43
|
getState: () => T;
|
|
31
44
|
};
|
|
32
45
|
interface OptionsObject {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import
|
|
1
|
+
import e from"react";import{useInsertionEffect as t}from"../useInsertionEffect.mjs";import{useSharedState as n}from"../useSharedState/useSharedState.mjs";import{useUrlEncode as o}from"../useUrlEncode/useUrlEncode.mjs";import{filterUnknownParamsClient as r}from"../utils.mjs";function s(s,c,i){const{parse:u,stringify:l}=o(s),{state:p,getState:d,setState:h}=n(s,(()=>i?.({parse:u})||s));t((()=>{const e=()=>{const e=u(r(s));h(e)};return window.addEventListener(a,e),()=>{window.removeEventListener(a,e)}}),[h]);const m=e.useRef([]),w=e.useCallback(((e,t)=>{const n="function"==typeof e?e(d()):e?{...d(),...e}:d(),o=l(n,function(e){const t=Object.keys(e),n=window.location.search,o=new URLSearchParams(n),r=new URLSearchParams;return o.forEach(((e,n)=>!t.includes(n)&&r.set(n,e))),r}(s)),r=`${window.location.pathname}${o.length?"?":""}${o}${window.location.hash}`;if(r===`${window.location.pathname}${window.location.search}${window.location.hash}`)return;let a;h(n);const i=t?.replace;delete t?.replace,m.current.push([i?"replace":"push",r,t]),1===m.current.length&&queueMicrotask((()=>{for(;m.current.length;){const e=m.current.shift();e&&e?.[1]!==a?.[1]&&(a=e)}const[e,t,n]=a||{};a=void 0,e&&c[e](t,n)}))}),[c,l,d]),f=e.useCallback((e=>{w(s,e)}),[w]);return{updateState:h,updateUrl:w,state:p,reset:f,getState:d}}const a="popstate";export{s as useUrlStateBase};
|
package/dist/utils.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export type Type = "string" | "date" | "boolean" | "number" | "bigint" | "undefi
|
|
|
8
8
|
* `function`, `symbol`, `array`
|
|
9
9
|
*/
|
|
10
10
|
export declare const typeOf: (val: unknown) => Type;
|
|
11
|
+
export declare const isPrimitive: (val: unknown) => val is Simple;
|
|
12
|
+
export type Simple = string | Date | boolean | number | null | undefined | Function | symbol;
|
|
11
13
|
export declare const isSSR: () => boolean;
|
|
12
14
|
export type JSON = null | boolean | Date | number | string | {
|
|
13
15
|
[prop: string]: JSON | JSON[];
|
|
@@ -22,6 +24,7 @@ export type UnknownObj = object | {
|
|
|
22
24
|
export declare const isEqual: (val1: unknown, val2: unknown) => boolean;
|
|
23
25
|
export declare function filterUnknownParamsClient<T extends object>(shape: T): string;
|
|
24
26
|
export declare function filterUnknownParams<T extends object>(shape: T, searchParams?: object): T;
|
|
27
|
+
export declare function filterUnknown<T extends object>(shape: T, entries: [key: string, value: string][]): string[][];
|
|
25
28
|
export declare function assignValue<T extends object>(shape: T, newVal: Partial<T>): T;
|
|
26
29
|
export interface Router {
|
|
27
30
|
push: (href: string, opts: object) => void;
|
package/dist/utils.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const t=t=>{const n=typeof t,e=null===t,r=Array.isArray(t),o=t instanceof Date;return(e?"null":o&&"date")||r&&"array"||!e&&!o&&!r&&"object"===n&&"object"||n},n=()=>"undefined"==typeof window,e=t=>new URLSearchParams("string"==typeof t?r(t):t?.toString?.()||""),r=t=>t.split("?")?.[1]||t||"",o=(t,n)=>JSON.stringify(t)===JSON.stringify(n);function i(t){const n=new URLSearchParams;return
|
|
1
|
+
const t=t=>{const n=typeof t,e=null===t,r=Array.isArray(t),o=t instanceof Date;return(e?"null":o&&"date")||r&&"array"||!e&&!o&&!r&&"object"===n&&"object"||n},n=()=>"undefined"==typeof window,e=t=>new URLSearchParams("string"==typeof t?r(t):t?.toString?.()||""),r=t=>t.split("?")?.[1]||t||"",o=(t,n)=>JSON.stringify(t)===JSON.stringify(n);function i(t){const n=new URLSearchParams;return c(t,[...new URLSearchParams(window.location.search).entries()]).forEach((([t,e])=>n.set(t,e))),n.toString()}function s(t,n){return Object.fromEntries(c(t,Object.entries(n||{})))}function c(t,n){const e=Object.keys(t),r=[];for(let t=0;t<n.length;t++)e.includes(n[t]?.[0])&&r.push([n[t][0].replace(/\+/g," "),n[t][1]]);return r}function a(t,n){const e=Object.assign({},t);return Object.entries(t).forEach((([r])=>{const o=r,i=void 0!==n[o];e[o]=i?n[o]:t[o]})),e}const u={push:t=>{window&&window.history.pushState(null,"",t)},replace:t=>{window&&window.history.replaceState(null,"",t)}};export{a as assignValue,c as filterUnknown,s as filterUnknownParams,i as filterUnknownParamsClient,e as getParams,o as isEqual,n as isSSR,u as routerHistory,t as typeOf};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "state-in-url",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.1.1",
|
|
4
4
|
"description": "Easily share complex state objects between unrelated React components, preserve types and structure, with TS validation. Deep links and url state synchronization wthout any hasssle or boilerplate.",
|
|
5
5
|
"homepage": "https://state-in-url.dev",
|
|
6
6
|
"repository": {
|
|
@@ -103,7 +103,7 @@
|
|
|
103
103
|
"reinstall": "npm run cleanup && npm install",
|
|
104
104
|
"setup": "playwright install --with-deps",
|
|
105
105
|
"prepack": "npm run build && cp package.json dist/package.json && npm run build:demo",
|
|
106
|
-
"pin-gh-deps": "pin-github-action .github/workflows/tests.yml",
|
|
106
|
+
"pin-gh-deps": "pin-github-action -i .github/workflows/tests.yml",
|
|
107
107
|
"prepare": "npx husky"
|
|
108
108
|
},
|
|
109
109
|
"wireit": {
|
|
@@ -342,7 +342,7 @@
|
|
|
342
342
|
"@testing-library/react": "^16.0.0",
|
|
343
343
|
"@testing-library/react-hooks": "^8.0.1",
|
|
344
344
|
"@types/glob": "^8.1.0",
|
|
345
|
-
"@types/jest": "^29.5.
|
|
345
|
+
"@types/jest": "^29.5.14",
|
|
346
346
|
"@types/node": "^20.11.24",
|
|
347
347
|
"@types/react": "^18.3.3",
|
|
348
348
|
"@types/react-dom": "^18.3.0",
|
|
@@ -374,7 +374,7 @@
|
|
|
374
374
|
"rollup-plugin-sourcemaps": "^0.6.3",
|
|
375
375
|
"semantic-release": "^24.1.0",
|
|
376
376
|
"standard-version": "^9.5.0",
|
|
377
|
-
"ts-jest": "^29.
|
|
377
|
+
"ts-jest": "^29.2.5",
|
|
378
378
|
"ts-node": "^10.9.2",
|
|
379
379
|
"typescript": "^5.3.3",
|
|
380
380
|
"wait-on": "^8.0.1",
|