state-in-url 4.1.0 → 4.1.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.
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!
@@ -47,18 +45,18 @@ Add a ⭐️ and <a href="https://github.com/asmyshlyaev177" target="_blank">fol
47
45
  Will appreciate you feedback/opinion on [discussions](https://github.com/asmyshlyaev177/state-in-url/discussions/1)
48
46
 
49
47
  Share if it useful for you.
50
- [FB](https://www.facebook.com/sharer.php?u=https://github.com/asmyshlyaev177/state-in-url)
51
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://github.com/asmyshlyaev177/state-in-url)
52
48
  [X.com](https://twitter.com/intent/tweet?&url=https://github.com/asmyshlyaev177/state-in-url)
49
+ [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://github.com/asmyshlyaev177/state-in-url)
50
+ [FB](https://www.facebook.com/sharer.php?u=https://github.com/asmyshlyaev177/state-in-url)
53
51
  [VK](http://vk.com/share.php?url=https://github.com/asmyshlyaev177/state-in-url)
54
52
 
55
53
  <hr />
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. All of it with keeping types and structure of data, e.g. numbers will be decoded as numbers not strings, dates as dates, etc, 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,28 @@ 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
+ - Sycn URL with React state
74
+ - Just sync data between unrelated client components without touching URI
75
+ - Shareable URLs with application state (Deep linking, URL state synchronization)
76
+ - Easy state persistence across page reloads
78
77
 
79
- # Features
78
+ ### Features
80
79
 
81
80
  - 🧩 **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
81
+ - 📘 **Typescript validation/autocomplete**: State is just an object, automatic static validation in IDE/tests according to Typescript definition
83
82
  - ✨ **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
83
  - ☂ **Default values**: Giving you default values if parameter not in url
86
84
  - ⌨ **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
85
+ - **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
86
+ - **Server Side Rendering**: Can use it in Server Components, Next.js 14 and 15 are supported
87
+ - **Lightweight**: Zero dependencies, library less than 2KB
88
+ - **DX**: Good developer experience, documentation, JSDoc comments, and examples
89
+ - **Framework Flexibility**: Hooks for `Next.js` and `react-router`, helpers to use it with other frameworks or pure JS
90
+ - **Well tested**: Unit tests and Playwright tests for Chrome/Firefox/Safari
91
+ - **Permissive license**: MIT
93
92
 
94
93
  ## Table of content
95
94
 
@@ -136,26 +135,26 @@ Possibly need to set `"module": "ES2022"`, or `"module": "ESNext"`
136
135
  Main hook that takes initial state as parameter and returns state object, callback to update url, and callback to update only state.
137
136
  All components that use the same `state` object are automatically synchronized.
138
137
 
139
- ## useUrlState hook for Next.js
138
+ ### useUrlState hook for Next.js
140
139
 
141
- [Full API Docs](packages/urlstate/next/useUrlState#api)
140
+ [Full API Docs](packages/urlstate/next/useUrlState)
142
141
 
143
142
  [React-Router example](#useurlstate-hook-for-react-router)
144
143
 
145
- ### Usage examples
144
+ #### Usage examples
146
145
 
147
- #### Basic
146
+ ##### Basic
148
147
 
149
148
  1. Define state shape with default values
150
149
 
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 }
150
+ ```typescript
151
+ // userState.ts
152
+ // Only parameters with value different from default will go to the url.
153
+ export const userState: UserState = { name: '', age: 0 }
155
154
 
156
- // use `Type` not `Interface`!
157
- type UserState = { name: string, age: number }
158
- ```
155
+ // use `Type` not `Interface`!
156
+ type UserState = { name: string, age: number }
157
+ ```
159
158
 
160
159
  2. Import it and use
161
160
 
@@ -196,7 +195,56 @@ function MyComponent() {
196
195
  }
197
196
  ```
198
197
 
199
- #### With complex state shape
198
+ ##### Custom hook to work with slice of state conveniently
199
+ <details>
200
+ <Summary>Example</Summary>
201
+
202
+ ```typescript
203
+ 'use client';
204
+
205
+ import React from 'react';
206
+ import { useUrlState } from 'state-in-url/next';
207
+
208
+ const form: Form = {
209
+ name: '',
210
+ age: undefined,
211
+ agree_to_terms: false,
212
+ tags: [],
213
+ };
214
+
215
+ type Form = {
216
+ name: string;
217
+ age?: number;
218
+ agree_to_terms: boolean;
219
+ tags: {id: string; value: {text: string; time: Date } }[];
220
+ };
221
+
222
+ export const useFormState = ({ searchParams }: { searchParams?: object }) => {
223
+ const { urlState, setUrl: setUrlBase, reset } = useUrlState(form, {
224
+ searchParams,
225
+ });
226
+
227
+ // first navigation will push new history entry
228
+ // all following will just replace that entry
229
+ // this way will have history with only 2 entries - ['/url', '/url?key=param']
230
+
231
+ const replace = React.useRef(false);
232
+ const setUrl = React.useCallback((
233
+ state: Parameters<typeof setUrlBase>[0],
234
+ opts?: Parameters<typeof setUrlBase>[1]
235
+ ) => {
236
+ setUrlBase(state, { replace: replace.current, ...opts });
237
+ replace.current = true;
238
+ }, [setUrlBase]);
239
+
240
+ return { urlState, setUrl, resetUrl: reset };
241
+ };
242
+ ```
243
+ </details>
244
+
245
+ <hr />
246
+
247
+ ##### With complex state shape
200
248
 
201
249
  <details>
202
250
  <Summary>Example</Summary>
@@ -276,7 +324,7 @@ const tags = [
276
324
  [Demo page example code](https://github.com/asmyshlyaev177/state-in-url/blob/master/packages/example-nextjs14/src/app/Form.tsx)
277
325
  </details>
278
326
 
279
- #### Update state only and sync to URL manually
327
+ ##### Update state only and sync to URL manually
280
328
 
281
329
  <details>
282
330
  <Summary>Example</Summary>
@@ -305,7 +353,7 @@ Syncing state `onBlur` will be more aligned with real world usage.
305
353
 
306
354
  </details>
307
355
 
308
- #### With server side rendering
356
+ ##### With server side rendering
309
357
 
310
358
  <details>
311
359
  <Summary>Example</Summary>
@@ -330,7 +378,7 @@ const Form = ({ searchParams }: { searchParams: object }) => {
330
378
 
331
379
  </details>
332
380
 
333
- #### Using hook in `layout` component
381
+ ##### Using hook in `layout` component
334
382
 
335
383
  <details>
336
384
  <Summary>Example</Summary>
@@ -381,7 +429,7 @@ export default async function Layout({
381
429
 
382
430
  </details>
383
431
 
384
- #### With arbitrary state shape (not recommended)
432
+ ##### With arbitrary state shape (not recommended)
385
433
 
386
434
  <details>
387
435
  <Summary>Example</Summary>
@@ -399,13 +447,13 @@ function SettingsComponent() {
399
447
 
400
448
  </details>
401
449
 
402
- ## useUrlState hook for React-Router
450
+ ### useUrlState hook for React-Router
403
451
 
404
452
  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.
405
453
 
406
- [API Docs](packages/urlstate/react-router/useUrlState#api)
454
+ [API Docs](packages/urlstate/react-router/useUrlState)
407
455
 
408
- ### Example
456
+ #### Example
409
457
 
410
458
  ```typescript
411
459
  export const form: Form = {
@@ -486,15 +534,15 @@ const tags = [
486
534
 
487
535
  [Example code](packages/example-react-router6/src/Form-for-test.tsx)
488
536
 
489
- # Other hooks and helpers
537
+ ## Other hooks and helpers
490
538
 
491
- ## `useUrlStateBase` hook for others routers
539
+ ### `useUrlStateBase` hook for others routers
492
540
 
493
541
  Hooks to create your own `useUrlState` hooks with other routers, e.g. react-router or tanstack router.
494
542
 
495
543
  [API Docs](packages/urlstate/useUrlStateBase)
496
544
 
497
- ## `useSharedState` hook for React.js
545
+ ### `useSharedState` hook for React.js
498
546
 
499
547
  Hook to share state between any React components, tested with Next.js and Vite.
500
548
 
@@ -511,19 +559,19 @@ function SettingsComponent() {
511
559
 
512
560
  [API Docs](packages/urlstate/useSharedState/README.md)
513
561
 
514
- ## `useUrlEncode` hook for React.js
562
+ ### `useUrlEncode` hook for React.js
515
563
 
516
564
  [API Docs](packages/urlstate/useUrlEncode/README.md)
517
565
 
518
- ## `encodeState` and `decodeState` helpers
566
+ ### `encodeState` and `decodeState` helpers
519
567
 
520
568
  [API Docs](packages/urlstate/encodeState/README.md)
521
569
 
522
- ## `encode` and `decode` helpers
570
+ ### `encode` and `decode` helpers
523
571
 
524
572
  [API Docs](packages/urlstate/encoder/README.md)
525
573
 
526
- # Best Practices
574
+ ## Best Practices
527
575
 
528
576
  - Define your state shape as a constant
529
577
  - Use TypeScript for enhanced type safety and autocomplete
@@ -557,9 +605,9 @@ export const useUserState = () => {
557
605
  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>
558
606
  3. Tested with `next.js` 14/15 with app router, no plans to support pages.
559
607
 
560
- # Other
608
+ ## Other
561
609
 
562
- ## Contribute and/or run locally
610
+ ### Contribute and/or run locally
563
611
 
564
612
  See [Contributing doc](CONTRIBUTING.md)
565
613
 
@@ -2,3 +2,4 @@ export declare const SYMBOLS: {
2
2
  readonly date: "⏲";
3
3
  readonly undefined: "∙undefined";
4
4
  };
5
+ export declare const TIMEOUT: number;
@@ -1 +1 @@
1
- const e={date:"⏲",undefined:"∙undefined"};export{e as SYMBOLS};
1
+ import{isSafari as e}from"../utils.mjs";const d={date:"⏲",undefined:"∙undefined"},n=e?330:70;export{d as SYMBOLS,n as TIMEOUT};
@@ -1 +1 @@
1
- export{SYMBOLS}from"./constants.mjs";
1
+ export{SYMBOLS,TIMEOUT}from"./constants.mjs";
@@ -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};
@@ -46,7 +46,7 @@ export declare function useUrlState<T extends JSONCompatible>({ defaultState: T,
46
46
  * // same as setState(form) with setUrl(form)
47
47
  * ```
48
48
  *
49
- * * 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}
50
50
  */
51
51
  export declare function useUrlState<T extends JSONCompatible>(defaultState: T, params?: Params): {
52
52
  urlState: T;
@@ -1 +1 @@
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
+ 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};
@@ -43,7 +43,7 @@ export declare function useUrlState<T extends JSONCompatible>({ defaultState: T,
43
43
  * // same as setState(form) with setUrl(form)
44
44
  * ```
45
45
  *
46
- * * 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}
47
47
  */
48
48
  export declare function useUrlState<T extends JSONCompatible>(defaultState: T, params?: Params): {
49
49
  urlState: T;
@@ -0,0 +1 @@
1
+ export declare const clone: (obj: object) => any;
@@ -1 +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),[a,i]=t.useState((()=>{if(u())return o?o?.():f.current;const t=r.get(f.current);if(t)return t;{const t=o?.()||f.current;return r.set(f.current,t),t}})),m=t.useCallback((t=>{const n=r.get(f.current),u="function"==typeof t?t(n):{...n,...t};c(n,u)||(r.set(f.current,u),e.get(f.current).forEach((t=>t())))}),[]);n((()=>{const t=e.add(f.current,(()=>{i(r.get(f.current)||f.current)}));return()=>{t()}}),[]);return{state:a,getState:t.useCallback((()=>r.get(f.current)||f.current),[]),setState:m}}export{s as useSharedState};
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),[a,i]=t.useState((()=>{if(u)return o?o?.():f.current;const t=r.get(f.current);if(t)return t;{const t=o?.()||f.current;return r.set(f.current,t),t}})),m=t.useCallback((t=>{const n=r.get(f.current),u="function"==typeof t?t(n):{...n,...t};c(n,u)||(r.set(f.current,u),e.get(f.current).forEach((t=>t())))}),[]);n((()=>{const t=e.add(f.current,(()=>{i(r.get(f.current)||f.current)}));return()=>{t()}}),[]);return{state:a,getState:t.useCallback((()=>r.get(f.current)||f.current),[]),setState:m}}export{s as useSharedState};
@@ -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
  /**
@@ -31,7 +31,7 @@ import { type JSONCompatible, type Router } from "../utils";
31
31
  *
32
32
  * ```
33
33
  *
34
- * * 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}
35
35
  */
36
36
  export declare function useUrlStateBase<T extends JSONCompatible>(defaultState: T, router: Router, getInitialState?: ({ parse, }: {
37
37
  parse: ReturnType<typeof useUrlEncode<T>>["parse"];
@@ -1 +1 @@
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};
1
+ import e from"react";import{TIMEOUT as t}from"../constants/constants.mjs";import{useInsertionEffect as o}from"../useInsertionEffect.mjs";import{useSharedState as r}from"../useSharedState/useSharedState.mjs";import{useUrlEncode as n}from"../useUrlEncode/useUrlEncode.mjs";import{filterUnknownParamsClient as s}from"../utils.mjs";function a(a,i,u){const{parse:m,stringify:l}=n(a),{state:p,getState:d,setState:f}=r(a,(()=>u?.({parse:m})||a)),h=e.useRef(void 0);o((()=>{const e=()=>{const e=m(s(a));f(e)};return window.addEventListener(c,e),()=>{window.removeEventListener(c,e),clearTimeout(h.current)}}),[f]);const w=e.useRef([]),S=e.useCallback(((e,o)=>{const r="function"==typeof e?e(d()):e?{...d(),...e}:d(),n=l(r,function(e){const t=Object.keys(e),o=window.location.search,r=new URLSearchParams(o),n=new URLSearchParams;return r.forEach(((e,o)=>!t.includes(o)&&n.set(o,e))),n}(a)),s=`${window.location.pathname}${n.length?"?":""}${n}${window.location.hash}`;if(s===`${window.location.pathname}${window.location.search}${window.location.hash}`)return;let c;f(r);const u=o?.replace;delete o?.replace,w.current.push([u?"replace":"push",s,o]),clearTimeout(h.current),h.current=setTimeout((()=>{queueMicrotask((()=>{for(;w.current.length;){const e=w.current.shift();e&&e?.[1]!==c?.[1]&&(c=e)}const[e,t,o]=c||{};c=void 0,e&&i[e](t,o)}))}),t)}),[i,l,d]),$=e.useCallback((e=>{S(a,e)}),[S]);return{updateState:f,updateUrl:S,state:p,reset:$,getState:d}}const c="popstate";export{a as useUrlStateBase};
package/dist/utils.d.ts CHANGED
@@ -8,7 +8,9 @@ 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 isSSR: () => boolean;
11
+ export declare const isPrimitive: (val: unknown) => val is Simple;
12
+ export type Simple = string | Date | boolean | number | null | undefined | Function | symbol;
13
+ export declare const isSSR: boolean;
12
14
  export type JSON = null | boolean | Date | number | string | {
13
15
  [prop: string]: JSON | JSON[];
14
16
  };
@@ -22,9 +24,11 @@ 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;
28
31
  replace: (href: string, opts: object) => void;
29
32
  }
30
33
  export declare const routerHistory: Router;
34
+ export declare const isSafari: boolean;
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 e=typeof t,n=null===t,r=Array.isArray(t),o=t instanceof Date;return(n?"null":o&&"date")||r&&"array"||!n&&!o&&!r&&"object"===e&&"object"||e},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);function i(t){const e=new URLSearchParams;return c(t,[...new URLSearchParams(window.location.search).entries()]).forEach((([t,n])=>e.set(t,n))),e.toString()}function s(t,e){return Object.fromEntries(c(t,Object.entries(e||{})))}function c(t,e){const n=Object.keys(t),r=[];for(let t=0;t<e.length;t++)n.includes(e[t]?.[0])&&r.push([e[t][0].replace(/\+/g," "),e[t][1]]);return r}function a(t,e){const n=Object.assign({},t);return Object.entries(t).forEach((([r])=>{const o=r,i=void 0!==e[o];n[o]=i?e[o]:t[o]})),n}const u={push:t=>{window&&window.history.pushState(null,"",t)},replace:t=>{window&&window.history.replaceState(null,"",t)}},l=/apple/i.test(!e&&navigator?.userAgent||"");export{a as assignValue,c as filterUnknown,s as filterUnknownParams,i as filterUnknownParamsClient,n as getParams,o as isEqual,e as isSSR,l as isSafari,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.1.0",
3
+ "version": "4.1.2",
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": {
@@ -202,6 +202,12 @@
202
202
  ],
203
203
  "command": "cd packages/example-nextjs14 && npm run build"
204
204
  },
205
+ "build:demo:tests": {
206
+ "dependencies": [
207
+ "build"
208
+ ],
209
+ "command": "cd packages/example-nextjs14 && npx cross-env TEST=true npm run build"
210
+ },
205
211
  "start:demo": {
206
212
  "dependencies": [
207
213
  "build:demo"
@@ -272,7 +278,7 @@
272
278
  "dependencies": [
273
279
  "build",
274
280
  "build:react",
275
- "build:demo",
281
+ "build:demo:tests",
276
282
  "build:next15",
277
283
  "build:router6"
278
284
  ]
@@ -342,7 +348,7 @@
342
348
  "@testing-library/react": "^16.0.0",
343
349
  "@testing-library/react-hooks": "^8.0.1",
344
350
  "@types/glob": "^8.1.0",
345
- "@types/jest": "^29.5.13",
351
+ "@types/jest": "^29.5.14",
346
352
  "@types/node": "^20.11.24",
347
353
  "@types/react": "^18.3.3",
348
354
  "@types/react-dom": "^18.3.0",
@@ -374,7 +380,7 @@
374
380
  "rollup-plugin-sourcemaps": "^0.6.3",
375
381
  "semantic-release": "^24.1.0",
376
382
  "standard-version": "^9.5.0",
377
- "ts-jest": "^29.1.5",
383
+ "ts-jest": "^29.2.5",
378
384
  "ts-node": "^10.9.2",
379
385
  "typescript": "^5.3.3",
380
386
  "wait-on": "^8.0.1",