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 CHANGED
@@ -89,7 +89,7 @@ pnpm add state-in-url
89
89
 
90
90
  ## Next.js的useUrlState钩子
91
91
 
92
- [文档](packages/urlstate/next/useUrlState#api)
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#api)
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="/assets/Logo_big.png?raw=true" alt="state-in-url logo"/>
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
  [![npm](https://img.shields.io/npm/v/state-in-url.svg)](https://www.npmjs.com/package/state-in-url)
21
- ![Tests](https://github.com/asmyshlyaev177/state-in-url/actions/workflows/tests.yml/badge.svg?branch=master)
15
+ ![npm bundle size (minified + gzip)](https://img.shields.io/bundlephobia/minzip/state-in-url.svg)
22
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
+
18
+ ![Tests](https://github.com/asmyshlyaev177/state-in-url/actions/workflows/tests.yml/badge.svg?branch=master)
23
19
  [![Codacy Badge](https://app.codacy.com/project/badge/Coverage/73be54068b7f41b0b74a252579ac09ec)](https://app.codacy.com/gh/asmyshlyaev177/state-in-url/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_coverage)
24
20
  [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](https://github.com/asmyshlyaev177/state-in-url/)
25
21
  [![semantic-release: angular](https://img.shields.io/badge/semantic--release-angular-e10079?logo=semantic-release)]([https://github.com/semantic-release/semantic-release](https://github.com/asmyshlyaev177/state-in-url))
26
- ![npm bundle size (minified + gzip)](https://img.shields.io/bundlephobia/minzip/state-in-url.svg)
22
+
27
23
  [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/asmyshlyaev177/state-in-url/badge)](https://scorecard.dev/viewer/?uri=github.com/asmyshlyaev177/state-in-url)
28
24
  [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/9679/badge)](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
- # Why use `state-in-url`?
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, Date stay a date, objects and arrays supported.
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
- # Use cases
70
+ ### Use cases
73
71
 
74
- - 💾 Store unsaved user forms or page filters in URL
75
- - 🧠 Just sync data between unrelated client components without touching URI
76
- - 🔗 Shareable URLs with application state (Deep linking, URL state synchronization)
77
- - 🔄 Easy state persistence across page reloads
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
- # Features
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
- - ⚙ **Well tested**: Unit tests and Playwright tests for Chrome/Firefox/Safari
88
- - ⚡ **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
89
- - 🪶 **Lightweight**: Zero dependencies, library less than 2KB
90
- - 👍 **DX**: Good developer experience, documentation, JSDoc comments, and examples
91
- - ⚛️ **Framework Flexibility**: Hooks for `Next.js` and `react-router`, helpers to use it with other frameworks or pure JS
92
- - 📃 **Permissive license**: MIT
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
- ## useUrlState hook for Next.js
137
+ ### useUrlState hook for Next.js
140
138
 
141
- [Full API Docs](packages/urlstate/next/useUrlState#api)
139
+ [Full API Docs](packages/urlstate/next/useUrlState)
142
140
 
143
141
  [React-Router example](#useurlstate-hook-for-react-router)
144
142
 
145
- ### Usage examples
143
+ #### Usage examples
146
144
 
147
- #### Basic
145
+ ##### Basic
148
146
 
149
147
  1. Define state shape with default values
150
148
 
151
- ```typescript
152
- // userState.ts
153
- // Only parameters with value different from default will go to the url.
154
- export const userState: UserState = { name: '', age: 0 }
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
- // use `Type` not `Interface`!
157
- type UserState = { name: string, age: number }
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={() => setUrl(userState)}>
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
- #### With complex state shape
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
- #### Update state only and sync to URL manually
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
- #### With server side rendering
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
- #### Using hook in `layout` component
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
- #### With arbitrary state shape (not recommended)
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
- ## useUrlState hook for React-Router
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#api)
453
+ [API Docs](packages/urlstate/react-router/useUrlState)
406
454
 
407
- ### Example
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
- # Other hooks and helpers
536
+ ## Other hooks and helpers
489
537
 
490
- ## `useUrlStateBase` hook for others routers
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
- ## `useSharedState` hook for React.js
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
- ## `useUrlEncode` hook for React.js
561
+ ### `useUrlEncode` hook for React.js
514
562
 
515
563
  [API Docs](packages/urlstate/useUrlEncode/README.md)
516
564
 
517
- ## `encodeState` and `decodeState` helpers
565
+ ### `encodeState` and `decodeState` helpers
518
566
 
519
567
  [API Docs](packages/urlstate/encodeState/README.md)
520
568
 
521
- ## `encode` and `decode` helpers
569
+ ### `encode` and `decode` helpers
522
570
 
523
571
  [API Docs](packages/urlstate/encoder/README.md)
524
572
 
525
- # Best Practices
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
- # Other
607
+ ## Other
560
608
 
561
- ## Contribute and/or run locally
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 type Primitive = Exclude<ReturnType<typeof decodePrimitive>, typeof errorSym>;
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): JSONCompatible | Primitive;
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 | Primitive | undefined} - The parsed object or a primitive value, or undefined if parsing fails.
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 | Primitive | undefined;
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>;
@@ -1 +1 @@
1
- import{SYMBOLS as t}from"../constants/constants.mjs";import{typeOf as e}from"../utils.mjs";function n(n){if(u(n))return n;switch(e(n)){case"function":case"symbol":return"";case"date":return r=n,t.date+new Date(r).toISOString();case"undefined":return t.undefined;default:return JSON.stringify(n).replace(/'/g,"%27").replace(/"/g,"'")}var r}function r(t,e){return a(t.replace(/'/g,'"').replace(/%27/g,"'"),e)}const s=e=>{if(e!==t.undefined)return e?.startsWith?.(t.date)?new Date(e.slice(1)):c},c=Symbol("isError"),i=(t,e)=>{const n="string"==typeof e,r=n&&s(e);return r===c?e:n?r:e};function a(t,e){try{return JSON.parse(t,i)}catch{const n=s(t);return n!==c?n:e}}const o=new RegExp(`^(${t.undefined}|${t.date})`),u=t=>o.test(String(t));export{r as decode,s as decodePrimitive,n as encode,c as errorSym,a as parseJSON,i as reviver};
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 of 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
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#api}
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 t,useSearchParams as e}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,m){const f="defaultState"in i?i.defaultState:i,n="defaultState"in i?i.searchParams:m?.searchParams,S="defaultState"in i?i.useHistory:m?.useHistory,d="defaultState"in i?{scroll:i.scroll,replace:i.replace}:{scroll:m?.scroll,replace:m?.replace},j=void 0===S||S?l:t(),{state:U,updateState:b,updateUrl:g,getState:P}=s(f,j,(({parse:t})=>o()?r(c(f,n),f):t(u(f)))),h=a.useCallback(((t,e)=>g(t,{...p,...d,...e})),[g,d]),v=e();return a.useEffect((()=>{b(c(f,r(Object.fromEntries([...v.entries()]),f)))}),[v]),{setState:b,updateState:b,setUrl:h,updateUrl:h,urlState:U,state:U,getState:P}}const p={replace:!0,scroll:!1};export{i as useUrlState};
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};
@@ -1 +1 @@
1
- import{decodePrimitive as r,errorSym as t,decode as n}from"./encoder/encoder.mjs";function e(r,t){return{...t,...o(JSON.stringify(r),t)}}function o(n,e){try{return JSON.parse(n,c)}catch{const o=r(n);return o!==t?o:e}}const c=(r,t)=>{const e="string"==typeof t,o=e&&n(t?.replace?.(/'/g,'"'));return r&&e?o:t};export{e as parseSPObj};
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 of push, default `true`
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#api}
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=t(),j=e.useMemo((()=>i?l:{replace:(e,t)=>d(e,{...S,...f,...t}),push:(e,t)=>d(e,{...S,...f,...t})}),[d,f]),{state:v,updateState:R,updateUrl:U,getState:b}=s(n,j,(({parse:e})=>e(o(n)))),g=e.useCallback(((e,t)=>U(e,{...S,...f,...t})),[f]),[y]=r();return e.useEffect((()=>{R(p(n,u(n,a(Object.fromEntries([...y.entries()]),n))))}),[y]),{setState:R,updateState:R,setUrl:g,updateUrl:g,urlState:v,state:v,getState:b}}const S={replace:!0,preventScrollReset:!0};export{c as useUrlState};
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#api}
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} An object containing `state`, `getState`, `updateState`, and `updateUrl` properties.
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#api}
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 t from"react";import{useInsertionEffect as e}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));e((()=>{const t=()=>{const t=u(r(s));h(t)};return window.addEventListener(a,t),()=>{window.removeEventListener(a,t)}}),[h]);const m=t.useRef([]),w=t.useCallback(((t,e)=>{const n="function"==typeof t?t(d()):t?{...d(),...t}:d(),o=l(n,function(t){const e=Object.keys(t),n=window.location.search,o=new URLSearchParams(n),r=new URLSearchParams;return o.forEach(((t,n)=>!e.includes(n)&&r.set(n,t))),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=e?.replace;delete e?.replace,m.current.push([i?"replace":"push",r,e]),1===m.current.length&&queueMicrotask((()=>{for(;m.current.length;){const t=m.current.shift();t&&t?.[1]!==a?.[1]&&(a=t)}const[t,e,n]=a||{};a=void 0,t&&c[t](e,n)}))}),[c,l,d]);return{updateState:h,updateUrl:w,state:p,getState:d}}const a="popstate";export{s as useUrlStateBase};
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 s(t,[...new URLSearchParams(window.location.search).entries()]).forEach((([t,e])=>n.set(t,e))),n.toString()}function c(t,n){return Object.fromEntries(s(t,Object.entries(n||{})))}function s(t,n){const e=Object.keys(t);return n.filter((([t])=>e.includes(t))).map((([t,n])=>[t.replace(/\+/g," "),n]))}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 filterUnknownParams,i as filterUnknownParamsClient,e as getParams,o as isEqual,n as isSSR,u as routerHistory,t as typeOf};
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.0.9",
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.13",
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.1.5",
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",