state-in-url 0.2.6 → 0.2.7

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.md CHANGED
@@ -5,15 +5,13 @@
5
5
  <div align="center">
6
6
  <img src="/assets/logo.svg?raw=true" alt="Library logo"/>
7
7
 
8
- <div>Seamlessly store complex nested state object in URL query parameters with IDE autotomplete and TS validation.</div>
8
+ <div>Easily share state between unrelated React components, with IDE autocomplete and TS validation. Without any hasssle or boilerplate. </div>
9
9
  </div>
10
10
 
11
11
 
12
12
  <div align="center">
13
13
  <hr />
14
14
 
15
- [![For hire](/assets/hireBadge.svg)](https://www.linkedin.com/in/asmyshlyaev177/)
16
-
17
15
 
18
16
  [![npm](https://img.shields.io/npm/v/state-in-url.svg)](https://www.npmjs.com/package/state-in-url)
19
17
  ![Tests](https://github.com/asmyshlyaev177/state-in-url/actions/workflows/tests.yml/badge.svg?branch=main)
@@ -24,12 +22,14 @@
24
22
 
25
23
  <div align="center">
26
24
 
27
- <h4 align="center">Don't hesitate to open issue if you found a bug</h4>
25
+ <h4 align="center">Don't hesitate to open an issue if you found a bug</h4>
28
26
 
29
27
  ![Demo-gif](https://github.com/asmyshlyaev177/state-in-url/assets/19854148/c9802601-4d42-4362-b3e4-37ff87c3b97f)
30
28
 
31
29
 
32
- <a href="https://state-in-url-asmyshlyaev177.vercel.app/" target="_blank">DEMO</a>
30
+ <a href="https://state-in-url-asmyshlyaev177.vercel.app/" target="_blank">DEMO</a> |
31
+ <a href="https://stackblitz.com/~/github.com/asmyshlyaev177/state-in-url?file=packages%2Fexample-nextjs%2Fsrc%2Fapp%2FForm.tsx" target="_blank">Demo on Stackblitz</a> |
32
+ <a href ="https://codesandbox.io/p/github/asmyshlyaev177/state-in-url/main?layout=%257B%2522sidebarPanel%2522%253A%2522EXPLORER%2522%252C%2522rootPanelGroup%2522%253A%257B%2522direction%2522%253A%2522horizontal%2522%252C%2522contentType%2522%253A%2522UNKNOWN%2522%252C%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522id%2522%253A%2522ROOT_LAYOUT%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522UNKNOWN%2522%252C%2522direction%2522%253A%2522vertical%2522%252C%2522id%2522%253A%2522clyk5bd9y00062v6jspcfrkx7%2522%252C%2522sizes%2522%253A%255B100%252C0%255D%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522EDITOR%2522%252C%2522direction%2522%253A%2522horizontal%2522%252C%2522id%2522%253A%2522EDITOR%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL%2522%252C%2522contentType%2522%253A%2522EDITOR%2522%252C%2522id%2522%253A%2522clyk5bd9x00022v6jyg71cr9e%2522%257D%255D%257D%252C%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522SHELLS%2522%252C%2522direction%2522%253A%2522horizontal%2522%252C%2522id%2522%253A%2522SHELLS%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL%2522%252C%2522contentType%2522%253A%2522SHELLS%2522%252C%2522id%2522%253A%2522clyk5bd9x00042v6jsos2y043%2522%257D%255D%257D%255D%257D%252C%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522DEVTOOLS%2522%252C%2522direction%2522%253A%2522vertical%2522%252C%2522id%2522%253A%2522DEVTOOLS%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL%2522%252C%2522contentType%2522%253A%2522DEVTOOLS%2522%252C%2522id%2522%253A%2522clyk5bd9x00052v6j5r632b12%2522%257D%255D%257D%255D%252C%2522sizes%2522%253A%255B50%252C50%255D%257D%252C%2522tabbedPanels%2522%253A%257B%2522clyk5bd9x00022v6jyg71cr9e%2522%253A%257B%2522tabs%2522%253A%255B%257B%2522id%2522%253A%2522clyk5bd9w00012v6j83rq3bvo%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522type%2522%253A%2522FILE%2522%252C%2522filepath%2522%253A%2522%252FREADME.md%2522%252C%2522state%2522%253A%2522IDLE%2522%257D%255D%252C%2522id%2522%253A%2522clyk5bd9x00022v6jyg71cr9e%2522%252C%2522activeTabId%2522%253A%2522clyk5bd9w00012v6j83rq3bvo%2522%257D%252C%2522clyk5bd9x00052v6j5r632b12%2522%253A%257B%2522id%2522%253A%2522clyk5bd9x00052v6j5r632b12%2522%252C%2522activeTabId%2522%253A%2522clyk5bdjx000b2v6jfc8ae464%2522%252C%2522tabs%2522%253A%255B%257B%2522type%2522%253A%2522SETUP_TASKS%2522%252C%2522id%2522%253A%2522clyk5bdjx000b2v6jfc8ae464%2522%252C%2522mode%2522%253A%2522permanent%2522%257D%252C%257B%2522type%2522%253A%2522UNASSIGNED_PORT%2522%252C%2522port%2522%253A2222%252C%2522id%2522%253A%2522clyk5cjbo004d2v6j3u55k74g%2522%252C%2522mode%2522%253A%2522permanent%2522%257D%252C%257B%2522type%2522%253A%2522ENV_SETUP%2522%252C%2522id%2522%253A%2522clyk5h8dp000r2v6j0r7kc7qq%2522%252C%2522mode%2522%253A%2522permanent%2522%257D%255D%257D%252C%2522clyk5bd9x00042v6jsos2y043%2522%253A%257B%2522id%2522%253A%2522clyk5bd9x00042v6jsos2y043%2522%252C%2522activeTabId%2522%253A%2522clyk5cbuv001q2v6joegrxxv6%2522%252C%2522tabs%2522%253A%255B%257B%2522id%2522%253A%2522clyk5bd9x00032v6jtfhj316o%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522type%2522%253A%2522NEW_TERMINAL%2522%257D%252C%257B%2522type%2522%253A%2522TASK_LOG%2522%252C%2522taskId%2522%253A%2522dev%2522%252C%2522id%2522%253A%2522clyk5cbuv001q2v6joegrxxv6%2522%252C%2522mode%2522%253A%2522permanent%2522%257D%252C%257B%2522type%2522%253A%2522TASK_LOG%2522%252C%2522taskId%2522%253A%2522cleanupDist%2522%252C%2522id%2522%253A%2522clyk5dgvo005i2v6jj950sxft%2522%252C%2522mode%2522%253A%2522permanent%2522%257D%255D%257D%257D%252C%2522showDevtools%2522%253Atrue%252C%2522showShells%2522%253Atrue%252C%2522showSidebar%2522%253Atrue%252C%2522sidebarPanelSize%2522%253A15%257D" target="_blank">Demo on Codesandbox</a>
33
33
 
34
34
 
35
35
  <hr />
@@ -42,17 +42,19 @@ Add a ⭐️ to support the project!
42
42
 
43
43
  ## Why use state-in-url?
44
44
 
45
- `state-in-url` lets you use URI string for state management.
45
+ `state-in-url` Simple state management with URL synchronization.
46
46
 
47
47
  # Use cases
48
+
49
+ - 🙃 Share the state between different components without changing url, good as alternative to signals and other state management tools
48
50
  - 🔗 Shareable URLs with full application state
49
51
  - 🔄 Easy state persistence across page reloads
50
- - 🧠 Pass data between unrelated client components
51
- - 🙃 Can just share the state without changing url
52
+ - 🧠 Sync data between unrelated client components
52
53
  - 🧮 Store unsaved user forms in URL
53
54
 
54
55
  # Features
55
- - 🧩 **Simple**: Handles complex objects without extra effort
56
+
57
+ - 🧩 **Simple**: No providers, reducers, boilerplate or new concepts
56
58
  - 📘 **Typescript support and type Safety**: Preserves data types and structure, enhances developer experience with IDE suggestions, strong typing and JSDoc comments
57
59
  - ⚛️ **Framework Flexibility**: Separate hooks for Next.js and React.js applications, and functions for pure JS
58
60
  - ⚙ **Well tested**: Unit tests and Playwright tests
@@ -61,6 +63,7 @@ Add a ⭐️ to support the project!
61
63
 
62
64
 
63
65
  ## Table of content
66
+
64
67
  - [Installation](#installation)
65
68
  - [`useUrlState` for Next.js](#useurlstate-hook-for-nextjs)
66
69
  - [`useUrlEncode` for React.js](#useurlencode-hook-for-reactjs)
@@ -75,7 +78,9 @@ Add a ⭐️ to support the project!
75
78
  - [Inspiration](#inspiration)
76
79
 
77
80
  ## installation
81
+
78
82
  ### 1. Install package
83
+
79
84
  ```sh
80
85
  # npm
81
86
  npm install --save state-in-url
@@ -84,20 +89,23 @@ yarn add state-in-url
84
89
  # pnpm
85
90
  pnpm add state-in-url
86
91
  ```
92
+
87
93
  ### 2. Edit tsconfig.json
88
- set `"moduleResolution": "Node16"` or `"moduleResolution": "NodeNext"` in your `tsconfig.json`
89
94
 
95
+ set `"moduleResolution": "Node16"` or `"moduleResolution": "NodeNext"` in your `tsconfig.json`
90
96
 
91
97
  ## useUrlState hook for Next.js
92
98
 
93
- `useUrlState` is a custom React hook for Next.js applications that make communication between client components easy. It allows you to store and retrieve state from the URL search parameters, providing a way to persist state across page reloads and share application state via URLs.
99
+ [Docs](packages/urlstate/useUrlState/README.md)
94
100
 
101
+ `useUrlState` is a custom React hook for Next.js applications that make communication between client components easy. It allows you to share any complex state and sync it with the URL search parameters, providing a way to persist state across page reloads and share application state via URLs.
95
102
 
96
103
  ### Usage examples
97
104
 
98
105
  #### Basic
99
106
 
100
107
  1. Define state shape
108
+
101
109
  ```typescript
102
110
  // countState.ts
103
111
  // State shape should be stored in a constant, don't pass an object directly
@@ -107,6 +115,7 @@ set `"moduleResolution": "Node16"` or `"moduleResolution": "NodeNext"` in your `
107
115
  ```
108
116
 
109
117
  2. Import it and use
118
+
110
119
  ```typescript
111
120
  'use client'
112
121
  import { useUrlState } from 'state-in-url';
@@ -248,79 +257,17 @@ function SettingsComponent() {
248
257
  }
249
258
  ```
250
259
 
251
- ## useUrlEncode hook for React.js
252
-
253
- `useUrlEncode` is a custom React hook that provides utility functions for encoding and decoding state object to and from URL search parameters. This hook doesn't depend on Nextjs, and will works with any React application.
254
-
255
- Accepts optional `defaultState` argument.
256
-
257
- ```typescript
258
- import { useUrlEncode } from 'state-in-url';
260
+ ## `useUrlEncode` hook for React.js
259
261
 
260
- const ageState = { age: 0 };
261
-
262
- const Component = () => {
263
- const { parse, stringify } = useUrlEncode();
264
-
265
- const str = stringify({ age: 36 }); // age=∓36
266
- const obj = parse(str, ageState); // { age: 36 }
267
- // Or const obj = parse(str); // { age: 36 }
268
-
269
- const currentParams = parse(window.location.search);
270
- // OR
271
- // const obj = parse(new URLSearchParams(window.location.search))
272
-
273
- const updateSearch = () => {
274
- const currentParams = new URLSearchParams(window.location.search);
275
- const newState = { query: 'react hooks', page: 2 };
276
- const updatedParamsString = stringify(newState, currentParams);
277
- console.log(updatedParamsString);
278
- // Output: existing params + query=react%20hooks&page=2
279
- };
280
- }
281
- ```
262
+ [Docs](packages/urlstate/useUrlEncode/README.md)
282
263
 
283
264
  ## `encodeState` and `decodeState` helpers
284
265
 
285
- `encodeState` let you encode some object with optional `defaults`, and optional existing queryString
286
- ```ts
287
- export const form = { name: '' };
288
- ...
289
- encodeState({ name: 'test' }, form, 'someExistingParam=123');
290
- ```
291
-
292
- `decodeState` let you decode queryString with optional defaults
293
- ```ts
294
- export const form = { name: '' };
295
- ...
296
- decodeState('name=Alex', form);
297
- ```
266
+ [Docs](packages/urlstate/encodeState/README.md)
298
267
 
299
268
  ## `encode` and `decode` helpers
300
269
 
301
- There low level helpers to stringify and parse query string params. Useful for other frameworks or pure JS.
302
-
303
- ```javascript
304
- import { encode, decode } from 'state-in-url';
305
-
306
- const state = { obj: [1, 2, 3], obj2: true }
307
-
308
- // to params
309
- const params = new URLSearchParams();
310
- Object.entries(state).forEach(([key, value]) => {
311
- params.set(key, encode(value));
312
- });
313
- const str = params.toString();
314
-
315
- // from params
316
- const obj = Object.fromEntries(
317
- [...params.entries()].map(([key, value]) => [
318
- key,
319
- // decode(value, optionalFallback),
320
- decode(value),
321
- ]),
322
- )
323
- ```
270
+ [Docs](packages/urlstate/encoder/README.md)
324
271
 
325
272
  ## Best Practices
326
273
 
@@ -335,10 +282,13 @@ const obj = Object.fromEntries(
335
282
  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
336
283
 
337
284
  ## Run locally
285
+
338
286
  Clone this repo, run `npm install` and
287
+
339
288
  ```sh
340
289
  npm run dev
341
290
  ```
291
+
342
292
  Go to [localhost:3000](http://localhost:3000)
343
293
 
344
294
 
@@ -349,9 +299,11 @@ Go to [localhost:3000](http://localhost:3000)
349
299
  ## [Changelog](https://github.com/asmyshlyaev177/state-in-url/blob/main/CHANGELOG.md)
350
300
 
351
301
  ## License
302
+
352
303
  This project is licensed under the [MIT license](https://github.com/asmyshlyaev177/state-in-url/blob/main/LICENSE).
353
304
 
354
305
  ## Inspiration
306
+
355
307
  [Using URL to store state in Vue ](https://dev.to/jacobandrewsky/using-url-to-store-state-in-vue-275c)
356
308
 
357
309
  [Storing state in the URL](https://antonz.org/storing-state/)
package/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0});var t=require("react"),e=require("next/navigation");function n(t){return t&&"object"==typeof t&&"default"in t?t:{default:t}}var r=n(t);const o=t=>{const e=null===t,n=Array.isArray(t),r=t instanceof Date;return(e?"null":r&&"date")||n&&"array"||!e&&!r&&!n&&"object"==typeof t&&"object"||typeof t},c=()=>"undefined"==typeof window,s=t=>new URLSearchParams("string"==typeof t?t:t?.toString?.()||""),u=(t,e)=>JSON.stringify(t)===JSON.stringify(e),a={string:"◖",boolean:"🗵",null:"∙null",undefined:"∙undefined",number:"∓",date:"⏲"};function i(t){switch(o(t)){case"function":case"symbol":return"";case"date":return a.date+t.toISOString();case"string":return t.match(/^◖/)?encodeURIComponent(t):`${a.string}${encodeURIComponent(t)}`;case"number":return a.number+String(t);case"boolean":return a.boolean+String(t);case"object":case"array":return JSON.stringify(t,f).replaceAll('"',"'");case"null":return a.null;case"undefined":return a.undefined;default:return String(t)}}function l(t,e){return function(t,e){try{return JSON.parse(t,g)}catch{const n=d(t);return n!==p?n:e}}(t.replaceAll("'",'"'),e)}const f=(t,e)=>{const n=o(e);return t&&"object"!==n&&"array"!==n?i(e):e},d=t=>t===a.null?null:t!==a.undefined?t?.startsWith?.(a.number)?Number.parseFloat(t.replace(a.number,"")):t?.startsWith?.(a.boolean)?!!t.includes("true"):t?.startsWith?.(a.date)?new Date(t.slice(1)):t?.startsWith?.(a.string)?decodeURIComponent(t).replace(/^◖/,""):p:void 0,p=Symbol("isError"),g=(t,e)=>{const n="string"==typeof e,r=n&&d(e);return r===p?e:t&&n?r:e};const y=(t,e)=>{const n="string"==typeof e,r=n&&l(e?.replaceAll?.("'",'"'));return t&&n?r:e};function b(t,e){try{return JSON.parse(t,y)}catch{const n=d(t);return n!==p?n:e}}function S(t,e,n){const r=s(n);return Object.entries(t||{}).forEach((([t,n])=>{const o=e?.[t];JSON.stringify(n)!==JSON.stringify(o)&&r.set(t,i(n))})),r.toString()}function w(t,e){return{...e||{},...Object.fromEntries([...s(t).entries()].map((([t,n])=>{const r=e?.[t];return[t,l(n,r)??r]})))}}function h(t){const e=r.default.useCallback((function(e,n){return"object"===o(e)?S(e,t,n):""}),[t]);return{parse:r.default.useCallback((function(e){return w(e,t)}),[t]),stringify:e}}const m=new WeakMap,O=new WeakMap,j={get:function(t){return m.get(t)},set:function(t,e){m.set(t,e)}};exports.decode=l,exports.decodeState=w,exports.encode=i,exports.encodeState=S,exports.useUrlEncode=h,exports.useUrlState=function(t,n){const{state:o,getState:s,setState:a,stringify:i}=function(t){const e=r.default.useRef(t),{parse:n,stringify:o}=h(e.current),[s,a]=r.default.useState((()=>{const t=j.get(e.current),r=n(c()?"":window.location.search);return t||(j.set(e.current,r),r)})),i=r.default.useCallback((t=>{const n=j.get(e.current)||e.current;if("function"==typeof t){const r=t(n);if(u(n,r))return;j.set(e.current,r),(O.get(e.current)||[]).forEach((t=>{t()}))}else{if(u(n,t))return;j.set(e.current,t),(O.get(e.current)||[]).forEach((t=>{t()}))}}),[]);return r.default.useInsertionEffect((()=>{const r=O.get(e.current)||[],o=()=>{a(j.get(e.current)||t)};O.set(e.current,r.concat(o));const c=()=>{const t=n(window.location.search);i(t)},s="popstate";return window.addEventListener(s,c),()=>{const t=O.get(e.current)||[];O.set(e.current,t.filter((t=>t!==o))),window.removeEventListener(s,c)}}),[i]),{state:s,getState:r.default.useCallback((()=>j.get(e.current)||e.current),[]),setState:i,stringify:o}}(c()?function(t,e){return{...e,...b(JSON.stringify(t),e)}}(n,t):t),l=e.useRouter();return{updateUrl:r.default.useCallback(((t,e)=>{const n=window.location.search,r=`${window.location.pathname}${n.length&&!n.includes("?")?"?":""}${n}`;let o;if("function"==typeof t){const e=t(s());o=i(e),a(e)}else{const e=t??s();o=i(e),a(e)}const c=`${window.location.pathname}${o.length?"?":""}${o}`;if(r!==c){const{replace:t,...n}=e||{};l[t?"replace":"push"](c,{scroll:!1,...n})}}),[l,i,s]),updateState:a,state:o}};
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0});var t=require("react"),e=require("next/navigation");function n(t){return t&&"object"==typeof t&&"default"in t?t:{default:t}}var r=n(t);const c={string:"◖",boolean:"🗵",null:"∙null",undefined:"∙undefined",number:"∓",date:"⏲"},o=t=>{const e=null===t,n=Array.isArray(t),r=t instanceof Date;return(e?"null":r&&"date")||n&&"array"||!e&&!r&&!n&&"object"==typeof t&&"object"||typeof t},s=()=>"undefined"==typeof window,u=t=>new URLSearchParams("string"==typeof t?t:t?.toString?.()||""),a=(t,e)=>JSON.stringify(t)===JSON.stringify(e);function i(t){switch(o(t)){case"function":case"symbol":return"";case"date":return c.date+t.toISOString();case"string":return t.match(/^◖/)?encodeURIComponent(t):`${c.string}${encodeURIComponent(t)}`;case"number":return c.number+String(t);case"boolean":return c.boolean+String(t);case"object":case"array":return JSON.stringify(t,f).replaceAll('"',"'");case"null":return c.null;case"undefined":return c.undefined;default:return String(t)}}function l(t,e){return function(t,e){try{return JSON.parse(t,g)}catch{const n=d(t);return n!==p?n:e}}(t.replaceAll("'",'"'),e)}const f=(t,e)=>{const n=o(e);return t&&"object"!==n&&"array"!==n?i(e):e},d=t=>t===c.null?null:t!==c.undefined?t?.startsWith?.(c.number)?Number.parseFloat(t.replace(c.number,"")):t?.startsWith?.(c.boolean)?!!t.includes("true"):t?.startsWith?.(c.date)?new Date(t.slice(1)):t?.startsWith?.(c.string)?decodeURIComponent(t).replace(/^◖/,""):p:void 0,p=Symbol("isError"),g=(t,e)=>{const n="string"==typeof e,r=n&&d(e);return r===p?e:t&&n?r:e};const y=(t,e)=>{const n="string"==typeof e,r=n&&l(e?.replaceAll?.("'",'"'));return t&&n?r:e};function b(t,e){try{return JSON.parse(t,y)}catch{const n=d(t);return n!==p?n:e}}function S(t,e,n){const r=u(n);return Object.entries(t||{}).forEach((([t,n])=>{const c=e?.[t];JSON.stringify(n)!==JSON.stringify(c)&&r.set(t,i(n))})),r.toString()}function w(t,e){return{...e||{},...Object.fromEntries([...u(t).entries()].map((([t,n])=>{const r=e?.[t];return[t,l(n,r)??r]})))}}function h(t){const e=r.default.useCallback((function(e,n){return"object"===o(e)?S(e,t,n):""}),[t]);return{parse:r.default.useCallback((function(e){return w(e,t)}),[t]),stringify:e}}const m=new WeakMap,O=new WeakMap,j={get:function(t){return m.get(t)},set:function(t,e){m.set(t,e)}};exports.decode=l,exports.decodeState=w,exports.encode=i,exports.encodeState=S,exports.useUrlEncode=h,exports.useUrlState=function(t,n){const{state:c,getState:o,setState:u,stringify:i}=function(t){const e=r.default.useRef(t),{parse:n,stringify:c}=h(e.current),[o,u]=r.default.useState((()=>{const t=j.get(e.current),r=n(s()?"":window.location.search);return t||(j.set(e.current,r),r)})),i=r.default.useCallback((t=>{const n=j.get(e.current)||e.current;if("function"==typeof t){const r=t(n);if(a(n,r))return;j.set(e.current,r),(O.get(e.current)||[]).forEach((t=>{t()}))}else{if(a(n,t))return;j.set(e.current,t),(O.get(e.current)||[]).forEach((t=>{t()}))}}),[]);return r.default.useInsertionEffect((()=>{const t=O.get(e.current)||[],r=()=>{u(j.get(e.current)||e.current)};O.set(e.current,t.concat(r));const c=()=>{const t=n(window.location.search);i(t)},o="popstate";return window.addEventListener(o,c),()=>{const t=O.get(e.current)||[];O.set(e.current,t.filter((t=>t!==r))),window.removeEventListener(o,c)}}),[i]),{state:o,getState:r.default.useCallback((()=>j.get(e.current)||e.current),[]),setState:i,stringify:c}}(s()?function(t,e){return{...e,...b(JSON.stringify(t),e)}}(n,t):t),l=e.useRouter(),f=r.default.useCallback(((t,e)=>{const n=window.location.search,r=`${window.location.pathname}${n.length&&!n.includes("?")?"?":""}${n}`;let c,s;"function"==typeof t?(c=t(o()),s=i(c)):(c=t??o(),s=i(c)),u(c);const a=`${window.location.pathname}${s.length?"?":""}${s}`;if(r!==a){const{replace:t,...n}=e||{};l[t?"replace":"push"](a,{scroll:!1,...n})}}),[l,i,o]);return{updateState:u,updateUrl:f,state:c}};
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- import t from"react";import{useRouter as e}from"next/navigation";const n=t=>{const e=null===t,n=Array.isArray(t),r=t instanceof Date;return(e?"null":r&&"date")||n&&"array"||!e&&!r&&!n&&"object"==typeof t&&"object"||typeof t},r=()=>"undefined"==typeof window,c=t=>new URLSearchParams("string"==typeof t?t:t?.toString?.()||""),o=(t,e)=>JSON.stringify(t)===JSON.stringify(e),s={string:"◖",boolean:"🗵",null:"∙null",undefined:"∙undefined",number:"∓",date:"⏲"};function a(t){switch(n(t)){case"function":case"symbol":return"";case"date":return s.date+t.toISOString();case"string":return t.match(/^◖/)?encodeURIComponent(t):`${s.string}${encodeURIComponent(t)}`;case"number":return s.number+String(t);case"boolean":return s.boolean+String(t);case"object":case"array":return JSON.stringify(t,i).replaceAll('"',"'");case"null":return s.null;case"undefined":return s.undefined;default:return String(t)}}function u(t,e){return function(t,e){try{return JSON.parse(t,g)}catch{const n=l(t);return n!==f?n:e}}(t.replaceAll("'",'"'),e)}const i=(t,e)=>{const r=n(e);return t&&"object"!==r&&"array"!==r?a(e):e},l=t=>t===s.null?null:t!==s.undefined?t?.startsWith?.(s.number)?Number.parseFloat(t.replace(s.number,"")):t?.startsWith?.(s.boolean)?!!t.includes("true"):t?.startsWith?.(s.date)?new Date(t.slice(1)):t?.startsWith?.(s.string)?decodeURIComponent(t).replace(/^◖/,""):f:void 0,f=Symbol("isError"),g=(t,e)=>{const n="string"==typeof e,r=n&&l(e);return r===f?e:t&&n?r:e};const d=(t,e)=>{const n="string"==typeof e,r=n&&u(e?.replaceAll?.("'",'"'));return t&&n?r:e};function p(t,e){try{return JSON.parse(t,d)}catch{const n=l(t);return n!==f?n:e}}function y(t,e,n){const r=c(n);return Object.entries(t||{}).forEach((([t,n])=>{const c=e?.[t];JSON.stringify(n)!==JSON.stringify(c)&&r.set(t,a(n))})),r.toString()}function b(t,e){return{...e||{},...Object.fromEntries([...c(t).entries()].map((([t,n])=>{const r=e?.[t];return[t,u(n,r)??r]})))}}function S(e){const r=t.useCallback((function(t,r){return"object"===n(t)?y(t,e,r):""}),[e]);return{parse:t.useCallback((function(t){return b(t,e)}),[e]),stringify:r}}const m=new WeakMap,w=new WeakMap,h={get:function(t){return m.get(t)},set:function(t,e){m.set(t,e)}};function O(n,c){const{state:s,getState:a,setState:u,stringify:i}=function(e){const n=t.useRef(e),{parse:c,stringify:s}=S(n.current),[a,u]=t.useState((()=>{const t=h.get(n.current),e=c(r()?"":window.location.search);return t||(h.set(n.current,e),e)})),i=t.useCallback((t=>{const e=h.get(n.current)||n.current;if("function"==typeof t){const r=t(e);if(o(e,r))return;h.set(n.current,r),(w.get(n.current)||[]).forEach((t=>{t()}))}else{if(o(e,t))return;h.set(n.current,t),(w.get(n.current)||[]).forEach((t=>{t()}))}}),[]);return t.useInsertionEffect((()=>{const t=w.get(n.current)||[],r=()=>{u(h.get(n.current)||e)};w.set(n.current,t.concat(r));const o=()=>{const t=c(window.location.search);i(t)},s="popstate";return window.addEventListener(s,o),()=>{const t=w.get(n.current)||[];w.set(n.current,t.filter((t=>t!==r))),window.removeEventListener(s,o)}}),[i]),{state:a,getState:t.useCallback((()=>h.get(n.current)||n.current),[]),setState:i,stringify:s}}(r()?function(t,e){return{...e,...p(JSON.stringify(t),e)}}(c,n):n),l=e();return{updateUrl:t.useCallback(((t,e)=>{const n=window.location.search,r=`${window.location.pathname}${n.length&&!n.includes("?")?"?":""}${n}`;let c;if("function"==typeof t){const e=t(a());c=i(e),u(e)}else{const e=t??a();c=i(e),u(e)}const o=`${window.location.pathname}${c.length?"?":""}${c}`;if(r!==o){const{replace:t,...n}=e||{};l[t?"replace":"push"](o,{scroll:!1,...n})}}),[l,i,a]),updateState:u,state:s}}export{u as decode,b as decodeState,a as encode,y as encodeState,S as useUrlEncode,O as useUrlState};
1
+ import t from"react";import{useRouter as e}from"next/navigation";const n={string:"◖",boolean:"🗵",null:"∙null",undefined:"∙undefined",number:"∓",date:"⏲"},r=t=>{const e=null===t,n=Array.isArray(t),r=t instanceof Date;return(e?"null":r&&"date")||n&&"array"||!e&&!r&&!n&&"object"==typeof t&&"object"||typeof t},c=()=>"undefined"==typeof window,o=t=>new URLSearchParams("string"==typeof t?t:t?.toString?.()||""),s=(t,e)=>JSON.stringify(t)===JSON.stringify(e);function a(t){switch(r(t)){case"function":case"symbol":return"";case"date":return n.date+t.toISOString();case"string":return t.match(/^◖/)?encodeURIComponent(t):`${n.string}${encodeURIComponent(t)}`;case"number":return n.number+String(t);case"boolean":return n.boolean+String(t);case"object":case"array":return JSON.stringify(t,i).replaceAll('"',"'");case"null":return n.null;case"undefined":return n.undefined;default:return String(t)}}function u(t,e){return function(t,e){try{return JSON.parse(t,g)}catch{const n=l(t);return n!==f?n:e}}(t.replaceAll("'",'"'),e)}const i=(t,e)=>{const n=r(e);return t&&"object"!==n&&"array"!==n?a(e):e},l=t=>t===n.null?null:t!==n.undefined?t?.startsWith?.(n.number)?Number.parseFloat(t.replace(n.number,"")):t?.startsWith?.(n.boolean)?!!t.includes("true"):t?.startsWith?.(n.date)?new Date(t.slice(1)):t?.startsWith?.(n.string)?decodeURIComponent(t).replace(/^◖/,""):f:void 0,f=Symbol("isError"),g=(t,e)=>{const n="string"==typeof e,r=n&&l(e);return r===f?e:t&&n?r:e};const d=(t,e)=>{const n="string"==typeof e,r=n&&u(e?.replaceAll?.("'",'"'));return t&&n?r:e};function p(t,e){try{return JSON.parse(t,d)}catch{const n=l(t);return n!==f?n:e}}function y(t,e,n){const r=o(n);return Object.entries(t||{}).forEach((([t,n])=>{const c=e?.[t];JSON.stringify(n)!==JSON.stringify(c)&&r.set(t,a(n))})),r.toString()}function b(t,e){return{...e||{},...Object.fromEntries([...o(t).entries()].map((([t,n])=>{const r=e?.[t];return[t,u(n,r)??r]})))}}function S(e){const n=t.useCallback((function(t,n){return"object"===r(t)?y(t,e,n):""}),[e]);return{parse:t.useCallback((function(t){return b(t,e)}),[e]),stringify:n}}const m=new WeakMap,w=new WeakMap,h={get:function(t){return m.get(t)},set:function(t,e){m.set(t,e)}};function O(n,r){const{state:o,getState:a,setState:u,stringify:i}=function(e){const n=t.useRef(e),{parse:r,stringify:o}=S(n.current),[a,u]=t.useState((()=>{const t=h.get(n.current),e=r(c()?"":window.location.search);return t||(h.set(n.current,e),e)})),i=t.useCallback((t=>{const e=h.get(n.current)||n.current;if("function"==typeof t){const r=t(e);if(s(e,r))return;h.set(n.current,r),(w.get(n.current)||[]).forEach((t=>{t()}))}else{if(s(e,t))return;h.set(n.current,t),(w.get(n.current)||[]).forEach((t=>{t()}))}}),[]);return t.useInsertionEffect((()=>{const t=w.get(n.current)||[],e=()=>{u(h.get(n.current)||n.current)};w.set(n.current,t.concat(e));const c=()=>{const t=r(window.location.search);i(t)},o="popstate";return window.addEventListener(o,c),()=>{const t=w.get(n.current)||[];w.set(n.current,t.filter((t=>t!==e))),window.removeEventListener(o,c)}}),[i]),{state:a,getState:t.useCallback((()=>h.get(n.current)||n.current),[]),setState:i,stringify:o}}(c()?function(t,e){return{...e,...p(JSON.stringify(t),e)}}(r,n):n),l=e(),f=t.useCallback(((t,e)=>{const n=window.location.search,r=`${window.location.pathname}${n.length&&!n.includes("?")?"?":""}${n}`;let c,o;"function"==typeof t?(c=t(a()),o=i(c)):(c=t??a(),o=i(c)),u(c);const s=`${window.location.pathname}${o.length?"?":""}${o}`;if(r!==s){const{replace:t,...n}=e||{};l[t?"replace":"push"](s,{scroll:!1,...n})}}),[l,i,a]);return{updateState:u,updateUrl:f,state:o}}export{u as decode,b as decodeState,a as encode,y as encodeState,S as useUrlEncode,O as useUrlState};
@@ -0,0 +1 @@
1
+ export * from './constants';
@@ -1,4 +1,4 @@
1
- import { type JSONCompatible, type UnknownObj } from './utils';
1
+ import { type JSONCompatible, type UnknownObj } from '../utils';
2
2
  /**
3
3
  * Encodes the state object into a URL query string.
4
4
  *
@@ -12,7 +12,7 @@ import { type JSONCompatible, type UnknownObj } from './utils';
12
12
  * encodeState({ name: 'test' }, form, 'someExistingParam=123');
13
13
  * ```
14
14
  *
15
- * * Github {@link https://github.com/asmyshlyaev177/state-in-url}
15
+ * * Github {@link https://github.com/asmyshlyaev177/state-in-url/tree/main/packages/urlstate/encodeState#encodestate}
16
16
  */
17
17
  export declare function encodeState<T extends JSONCompatible>(state: T, defaults?: T, paramsToKeep?: string | URLSearchParams): string;
18
18
  /**
@@ -27,6 +27,6 @@ export declare function encodeState<T extends JSONCompatible>(state: T, defaults
27
27
  * decodeState('key=value&name=Alex', form);
28
28
  * ```
29
29
  *
30
- * * Github {@link https://github.com/asmyshlyaev177/state-in-url}
30
+ * * Github {@link https://github.com/asmyshlyaev177/state-in-url/tree/main/packages/urlstate/encodeState#decodestate}
31
31
  */
32
32
  export declare function decodeState<T extends JSONCompatible>(uriString: string | URLSearchParams, defaults?: T): undefined extends T ? UnknownObj : T;
@@ -0,0 +1 @@
1
+ export * from './encodeState';
@@ -1,11 +1,11 @@
1
- import { type JSONCompatible } from './utils';
1
+ import { type JSONCompatible } from '../utils';
2
2
  /**
3
3
  * Encode any JSON serializable value to URL friendly string
4
4
  *
5
5
  * @param {unknown} payload any value to serialize
6
6
  * @returns {string} URLSearchParams compatible value string
7
7
  *
8
- * * Github {@link https://github.com/asmyshlyaev177/state-in-url}
8
+ * * Github {@link https://github.com/asmyshlyaev177/state-in-url/tree/main/packages/urlstate/encoder#encode}
9
9
  */
10
10
  export declare function encode(payload: unknown): string;
11
11
  /**
@@ -15,7 +15,7 @@ export declare function encode(payload: unknown): string;
15
15
  * @param {any} fallback optional fallback value
16
16
  * @returns {unknown} decoded object
17
17
  *
18
- * * Github {@link https://github.com/asmyshlyaev177/state-in-url}
18
+ * * Github {@link https://github.com/asmyshlyaev177/state-in-url/tree/main/packages/urlstate/encoder#decode}
19
19
  */
20
20
  export declare function decode<T>(payload: string, fallback?: T): JSONCompatible | Primitive;
21
21
  export declare const replacer: (key: string, value: unknown) => unknown;
@@ -0,0 +1 @@
1
+ export * from './encoder';
@@ -1,7 +1,7 @@
1
- import { encode, decode, parseSsrQs } from './encoder';
2
- import { type Type, type JSONCompatible, typeOf, isSSR } from './utils';
1
+ import { decode, encode, parseSsrQs } from './encoder';
2
+ import { decodeState, encodeState } from './encodeState';
3
3
  import { useUrlEncode } from './useUrlEncode';
4
4
  import { useUrlState } from './useUrlState';
5
- import { encodeState, decodeState } from './encodeState';
6
- export { encode, decode, typeOf, useUrlEncode, useUrlState, encodeState, decodeState, isSSR, parseSsrQs, };
7
- export type { Type, JSONCompatible };
5
+ import { isSSR, type JSONCompatible, type Type, typeOf } from './utils';
6
+ export { decode, decodeState, encode, encodeState, isSSR, parseSsrQs, typeOf, useUrlEncode, useUrlState, };
7
+ export type { JSONCompatible, Type };
@@ -2,6 +2,6 @@ import { type JSONCompatible } from './utils';
2
2
  export declare function useState<T extends JSONCompatible>(defaultState: T): {
3
3
  state: T;
4
4
  getState: () => T;
5
- setState: (value: T | ((currState: typeof defaultState) => typeof defaultState)) => void;
5
+ setState: (value: T | ((currState: T) => T)) => void;
6
6
  stringify: (state: T, paramsToKeep?: string | URLSearchParams | undefined) => string;
7
7
  };
@@ -0,0 +1 @@
1
+ export * from './useUrlEncode';
@@ -1,4 +1,4 @@
1
- import { type JSONCompatible } from './utils';
1
+ import { type JSONCompatible } from '../utils';
2
2
  /**
3
3
  * A hook that returns stringify and parse functions for encoding and decoding state
4
4
  * to and from URL search parameters.
@@ -17,9 +17,29 @@ import { type JSONCompatible } from './utils';
17
17
  * parse('name=Tom');
18
18
  * ```
19
19
  *
20
- * * Github {@link https://github.com/asmyshlyaev177/state-in-url}
20
+ * * Github {@link https://github.com/asmyshlyaev177/state-in-url/tree/main/packages/urlstate/useUrlEncode#api}
21
21
  */
22
22
  export declare function useUrlEncode<T extends JSONCompatible>(stateShape: T): {
23
+ /**
24
+ Parse URL query string to state object
25
+ * * Example:
26
+ * ```ts
27
+ const state = parse('name=◖Tom');
28
+ console.log(state); // Output: { name: 'Tom' }
29
+ * ```
30
+ *
31
+ * * Github {@link https://github.com/asmyshlyaev177/state-in-url/tree/main/packages/urlstate/useUrlEncode#parse}
32
+ */
23
33
  parse: (strOrSearchParams: string | URLSearchParams) => T;
34
+ /**
35
+ Converts a state object to a URL query string.
36
+ * * Example:
37
+ * ```ts
38
+ const queryString = stringify({ name: 'John' }, 'someExistingParamToKeep=123');
39
+ console.log(queryString); // Output: name=◖John&someExistingParamToKeep=123
40
+ * ```
41
+ *
42
+ * * Github {@link https://github.com/asmyshlyaev177/state-in-url/tree/main/packages/urlstate/useUrlEncode#stringify}
43
+ */
24
44
  stringify: (state: typeof stateShape, paramsToKeep?: string | URLSearchParams) => string;
25
45
  };
@@ -0,0 +1 @@
1
+ export * from './useUrlState';
@@ -1,5 +1,5 @@
1
1
  import { useRouter } from 'next/navigation';
2
- import { DeepReadonly, type JSONCompatible } from './utils';
2
+ import { type DeepReadonly, type JSONCompatible } from '../utils';
3
3
  /**
4
4
  * NextJS hook. Returns `state`, `updateState`, and `updateUrl` functions
5
5
  *
@@ -16,11 +16,31 @@ import { DeepReadonly, type JSONCompatible } from './utils';
16
16
  * updateUrl({ name: 'test' }, { replace: true, scroll: true });
17
17
  * ```
18
18
  *
19
- * * Github {@link https://github.com/asmyshlyaev177/state-in-url}
19
+ * * Github {@link https://github.com/asmyshlyaev177/state-in-url/tree/main/packages/urlstate/useUrlState#api}
20
20
  */
21
21
  export declare function useUrlState<T extends JSONCompatible>(defaultState: T, sp?: object): {
22
- updateUrl: (value?: T | ((currState: typeof defaultState) => typeof defaultState) | undefined, options?: Options) => void;
22
+ /**
23
+ * * Example:
24
+ * ```ts
25
+ * updateState({ name: 'test' });
26
+ * // or
27
+ * updateState(curr => ({ ...curr, name: 'test' }) );
28
+ * ```
29
+ *
30
+ * * Github {@link https://github.com/asmyshlyaev177/state-in-url/tree/main/packages/urlstate/useUrlState#updatestate}
31
+ */
23
32
  updateState: (value: T | ((currState: T) => T)) => void;
33
+ /**
34
+ * * Example:
35
+ * ```ts
36
+ * updateUrl({ name: 'test' });
37
+ * // or
38
+ * updateUrl(curr => ({ ...curr, name: 'test' }), { replace: true, scroll: false } );
39
+ * ```
40
+ *
41
+ * * Github {@link https://github.com/asmyshlyaev177/state-in-url/tree/main/packages/urlstate/useUrlState#updateurl}
42
+ */
43
+ updateUrl: (value?: T | DeepReadonly<T> | ((currState: T) => T) | undefined, options?: Options) => void;
24
44
  state: DeepReadonly<T>;
25
45
  };
26
46
  type Router = ReturnType<typeof useRouter>;
@@ -15,7 +15,7 @@ export type JSON = null | boolean | Date | number | string | {
15
15
  export type JSONCompatible = {
16
16
  [prop: string]: JSON | JSON[];
17
17
  };
18
- export type DeepReadonly<T> = T extends Map<infer K, infer V> ? ReadonlyMap<DeepReadonly<K>, DeepReadonly<V>> : T extends Set<infer S> ? ReadonlySet<DeepReadonly<S>> : T extends JSONCompatible ? {
18
+ export type DeepReadonly<T> = T extends Map<infer K, infer V> ? ReadonlyMap<DeepReadonly<K>, DeepReadonly<V>> : T extends Set<infer S> ? ReadonlySet<DeepReadonly<S>> : T extends object ? {
19
19
  readonly [K in keyof T]: DeepReadonly<T[K]>;
20
20
  } : T;
21
21
  export declare const getParams: (strOrSearchParams?: string | URLSearchParams) => URLSearchParams;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "state-in-url",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "description": "Library to store state in browser URL, includes hook for NextJS, hook for pure React, and low level helpers.",
5
5
  "homepage": "https://github.com/asmyshlyaev177/state-in-url#readme",
6
6
  "keywords": [
@@ -70,6 +70,7 @@
70
70
  "test:int": "playwright test",
71
71
  "test:int:watch": "npx chokidar-cli 'packages/**/*.ts' 'packages/**/*.tsx' -i '.next' -t 500 -c 'npx playwright test -j 2 --project=chromium'",
72
72
  "test": "wireit",
73
+ "tsc": "wireit",
73
74
  "dev": "wireit",
74
75
  "dev:lib": "wireit",
75
76
  "dev:nextjs": "wireit",
@@ -87,13 +88,20 @@
87
88
  "wireit": {
88
89
  "test": {
89
90
  "dependencies": [
91
+ "tsc",
90
92
  "build",
91
93
  "test:unit",
92
94
  "dev:nextjs"
93
95
  ],
94
96
  "command": "npx playwright test --project=chromium -j 2"
95
97
  },
98
+ "tsc": {
99
+ "command": "npx tsc --noEmit"
100
+ },
96
101
  "test:unit": {
102
+ "dependencies": [
103
+ "tsc"
104
+ ],
97
105
  "command": "npx jest"
98
106
  },
99
107
  "dev": {
@@ -177,7 +185,7 @@
177
185
  "cz": "^1.8.2",
178
186
  "cz-conventional-changelog": "^3.3.0",
179
187
  "eslint": "^8.57.0",
180
- "eslint-plugin-maintainable": "^0.3.1",
188
+ "eslint-plugin-maintainable": "^0.4.4",
181
189
  "husky": "^9.0.11",
182
190
  "jest": "^29.7.0",
183
191
  "jest-environment-jsdom": "^29.7.0",