state-in-url 0.2.6 → 1.0.0

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,13 @@
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://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
32
 
34
33
 
35
34
  <hr />
@@ -42,17 +41,19 @@ Add a ⭐️ to support the project!
42
41
 
43
42
  ## Why use state-in-url?
44
43
 
45
- `state-in-url` lets you use URI string for state management.
44
+ `state-in-url` Simple state management with URL synchronization.
46
45
 
47
46
  # Use cases
47
+
48
+ - 🙃 Share the state between different components without changing url, good as alternative to signals and other state management tools
48
49
  - 🔗 Shareable URLs with full application state
49
50
  - 🔄 Easy state persistence across page reloads
50
- - 🧠 Pass data between unrelated client components
51
- - 🙃 Can just share the state without changing url
51
+ - 🧠 Sync data between unrelated client components
52
52
  - 🧮 Store unsaved user forms in URL
53
53
 
54
54
  # Features
55
- - 🧩 **Simple**: Handles complex objects without extra effort
55
+
56
+ - 🧩 **Simple**: No providers, reducers, boilerplate or new concepts
56
57
  - 📘 **Typescript support and type Safety**: Preserves data types and structure, enhances developer experience with IDE suggestions, strong typing and JSDoc comments
57
58
  - ⚛️ **Framework Flexibility**: Separate hooks for Next.js and React.js applications, and functions for pure JS
58
59
  - ⚙ **Well tested**: Unit tests and Playwright tests
@@ -61,6 +62,7 @@ Add a ⭐️ to support the project!
61
62
 
62
63
 
63
64
  ## Table of content
65
+
64
66
  - [Installation](#installation)
65
67
  - [`useUrlState` for Next.js](#useurlstate-hook-for-nextjs)
66
68
  - [`useUrlEncode` for React.js](#useurlencode-hook-for-reactjs)
@@ -75,7 +77,9 @@ Add a ⭐️ to support the project!
75
77
  - [Inspiration](#inspiration)
76
78
 
77
79
  ## installation
80
+
78
81
  ### 1. Install package
82
+
79
83
  ```sh
80
84
  # npm
81
85
  npm install --save state-in-url
@@ -84,20 +88,23 @@ yarn add state-in-url
84
88
  # pnpm
85
89
  pnpm add state-in-url
86
90
  ```
91
+
87
92
  ### 2. Edit tsconfig.json
88
- set `"moduleResolution": "Node16"` or `"moduleResolution": "NodeNext"` in your `tsconfig.json`
89
93
 
94
+ set `"moduleResolution": "Node16"` or `"moduleResolution": "NodeNext"` in your `tsconfig.json`
90
95
 
91
96
  ## useUrlState hook for Next.js
92
97
 
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.
98
+ [Docs](packages/urlstate/useUrlState/README.md)
94
99
 
100
+ `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
101
 
96
102
  ### Usage examples
97
103
 
98
104
  #### Basic
99
105
 
100
106
  1. Define state shape
107
+
101
108
  ```typescript
102
109
  // countState.ts
103
110
  // State shape should be stored in a constant, don't pass an object directly
@@ -107,6 +114,7 @@ set `"moduleResolution": "Node16"` or `"moduleResolution": "NodeNext"` in your `
107
114
  ```
108
115
 
109
116
  2. Import it and use
117
+
110
118
  ```typescript
111
119
  'use client'
112
120
  import { useUrlState } from 'state-in-url';
@@ -248,79 +256,17 @@ function SettingsComponent() {
248
256
  }
249
257
  ```
250
258
 
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';
259
+ ## `useUrlEncode` hook for React.js
259
260
 
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
- ```
261
+ [Docs](packages/urlstate/useUrlEncode/README.md)
282
262
 
283
263
  ## `encodeState` and `decodeState` helpers
284
264
 
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
- ```
265
+ [Docs](packages/urlstate/encodeState/README.md)
298
266
 
299
267
  ## `encode` and `decode` helpers
300
268
 
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
- ```
269
+ [Docs](packages/urlstate/encoder/README.md)
324
270
 
325
271
  ## Best Practices
326
272
 
@@ -328,6 +274,7 @@ const obj = Object.fromEntries(
328
274
  - Use TypeScript for enhanced type safety and autocomplete
329
275
  - Avoid storing sensitive information in URL parameters
330
276
  - Use `updateState` for frequent updates and `updateUrl` to sync changes to url
277
+ - Use this [extension](https://marketplace.visualstudio.com/items?itemName=yoavbls.pretty-ts-errors) for readable TS errors
331
278
 
332
279
  ## Gothas
333
280
 
@@ -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`${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 u(t){switch(r(t)){case"function":case"symbol":return"";case"date":return n.date+t.toISOString();case"string":return`${n.string}${encodeURIComponent(t)}`;case"number":return n.number+String(t);case"boolean":return n.boolean+String(t);case"object":case"array":return JSON.stringify(t,i).replaceAll('"',"'");case"null":return n.null;case"undefined":return n.undefined;default:return String(t)}}function a(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?u(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&&a(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,u(n))})),r.toString()}function b(t,e){return{...e||{},...Object.fromEntries([...o(t).entries()].map((([t,n])=>{const r=e?.[t];return[t,a(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 w=new WeakMap,m=new WeakMap,h={get:function(t){return w.get(t)},set:function(t,e){w.set(t,e)}};function O(n,r){const{state:o,getState:u,setState:a,stringify:i}=function(e){const n=t.useRef(e),{parse:r,stringify:o}=S(n.current),[u,a]=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),(m.get(n.current)||[]).forEach((t=>{t()}))}else{if(s(e,t))return;h.set(n.current,t),(m.get(n.current)||[]).forEach((t=>{t()}))}}),[]);return t.useInsertionEffect((()=>{const t=m.get(n.current)||[],e=()=>{a(h.get(n.current)||n.current)};m.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=m.get(n.current)||[];m.set(n.current,t.filter((t=>t!==e))),window.removeEventListener(o,c)}}),[i]),{state:u,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(u()),o=i(c)):(c=t??u(),o=i(c)),a(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,u]);return{updateState:a,updateUrl:f,state:o}}export{a as decode,b as decodeState,u 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 };
@@ -1,7 +1,7 @@
1
- import { type JSONCompatible } from './utils';
1
+ import { type DeepReadonly, 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 | DeepReadonly<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;
23
- updateState: (value: T | ((currState: T) => T)) => 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
+ */
32
+ updateState: (value: T | DeepReadonly<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": "1.0.0",
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",