state-in-url 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,12 +2,15 @@
2
2
  <h1 align="center">state-in-url</h1>
3
3
 
4
4
 
5
- <p align="center">
5
+ <div align="center">
6
6
  <img src="/assets/logo.svg?raw=true" alt="Library logo"/>
7
- </p>
7
+
8
+ <div>Seamlessly store complex nested state object in URL query parameters with IDE autotomplete and TS validation.</div>
9
+ </div>
8
10
 
9
11
 
10
12
  <div align="center">
13
+ <hr />
11
14
 
12
15
  [![For hire](/assets/hireBadge.svg)](https://www.linkedin.com/in/asmyshlyaev177/)
13
16
 
@@ -19,7 +22,6 @@
19
22
  </div>
20
23
 
21
24
  <div align="center">
22
- Seamlessly store complex React state object in URL query parameters without losing types. Works with Next.js/React.js applications.
23
25
 
24
26
  <h4 align="center">Don't hesitate to open issue if you found a bug</h4>
25
27
 
@@ -46,12 +48,14 @@ Add a ⭐️ to support the project!
46
48
  - 🔗 Shareable URLs with full application state
47
49
  - 🔄 Easy state persistence across page reloads
48
50
  - 🧠 Pass data between unrelated client components
51
+ - 🙃 Can just share the state without changing url
49
52
  - 🧮 Store unsaved user forms in URL
50
53
 
51
54
  # Features
52
55
  - 🧩 **Simple**: Handles complex objects without extra effort
53
- - 📘 **Typescript support and type Safety**: Preserves data types and structure, enhances developer experience with strong typing and JSDoc comments
56
+ - 📘 **Typescript support and type Safety**: Preserves data types and structure, enhances developer experience with IDE suggestions, strong typing and JSDoc comments
54
57
  - ⚛️ **Framework Flexibility**: Separate hooks for Next.js and React.js applications, and functions for pure JS
58
+ - ⚙ **Well tested**: Unit tests and Playwright tests
55
59
  - 🪶 **Lightweight**: Zero dependencies for a smaller footprint
56
60
 
57
61
 
@@ -113,11 +117,13 @@ function MyComponent() {
113
117
  Increment (Update URL)
114
118
  </button>
115
119
 
116
- <button onClick={() => updateState({ count: state.count + 1 })}>
120
+ // same api as React.useState
121
+ <button onClick={() => updateState(currState => ({...currState, count: currState.count + 1 }) )}>
117
122
  Increment (Local Only)
118
123
  </button>
119
124
  <button onClick={() => updateUrl()}>
120
125
  Sync changes to url
126
+ // Or don't sync it and just share state
121
127
  </button>
122
128
 
123
129
  <button onClick={() => updateUrl(state)}>
@@ -157,6 +163,20 @@ function SettingsComponent() {
157
163
  }));
158
164
  };
159
165
 
166
+ // sync state to url when idle
167
+ const timer = React.useRef(0 as unknown as NodeJS.Timeout);
168
+ React.useEffect(() => {
169
+ clearTimeout(timer.current);
170
+ timer.current = setTimeout(() => {
171
+ // will compare state by content not by reference and fire update only for new values
172
+ updateUrl(state);
173
+ }, 500);
174
+
175
+ return () => {
176
+ clearTimeout(timer.current);
177
+ };
178
+ }, [state, updateUrl]);
179
+
160
180
  return (
161
181
  <div>
162
182
  <h2>User Settings</h2>
package/dist/index.js 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};function p(t,e){return{...e,...y(JSON.stringify(t),e)}}const d=(t,e)=>{const n="string"==typeof e,r=n&&u(e?.replaceAll?.("'",'"'));return r===f?e:t&&n?r:e};function y(t,e){try{return JSON.parse(t,d)}catch{const n=l(t);return n!==f?n:e}}function b(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 S(t,e){return{...e||{},...Object.fromEntries([...c(t).entries()].map((([t,n])=>{const r=e?.[t];return[t,u(n,r)??r]})))}}function m(e){const r=t.useCallback((function(t,r){return"object"===n(t)?b(t,e,r):""}),[e]);return{parse:t.useCallback((function(t){return S(t,e)}),[e]),stringify:r}}const h=new WeakMap,w=new WeakMap;function O(n,c){const{state:s,getState:a,setState:u,stringify:i}=function(e){const n=t.useRef(e),{parse:c,stringify:s}=m(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}));return t.useInsertionEffect((()=>{const t=w.get(n.current)||[],r=()=>{u(h.get(n.current)||e)};return w.set(n.current,t.concat(r)),()=>{const t=w.get(n.current)||[];w.set(n.current,t.filter((t=>t!==r)))}}),[]),{state:a,getState:t.useCallback((()=>h.get(n.current)||n.current),[]),setState:t.useCallback((t=>{const e=h.get(n.current)||n.current;if("function"==typeof t){const r=t(e);if(o(e,r))return!1;h.set(n.current,r),(w.get(n.current)||[]).forEach((t=>{t()}))}else{if(o(e,t))return!1;h.set(n.current,t),(w.get(n.current)||[]).forEach((t=>{t()}))}}),[]),parse:c,stringify:s}}(n),l=e();return{updateUrl:t.useCallback(((t,e)=>{const n=window.location.search,r=`${window.location.pathname}${n.length?"?":""}${n}`,c=i("function"==typeof t?t(a()):a()),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:r()?p(c,n):s}}export{u as decode,S as decodeState,a as encode,b as encodeState,m as useUrlEncode,O as useUrlState};
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 p=(t,e)=>{const n="string"==typeof e,r=n&&u(e?.replaceAll?.("'",'"'));return r===f?e:t&&n?r:e};function d(t,e){try{return JSON.parse(t,p)}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,h=new WeakMap;function w(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=m.get(n.current),e=c(r()?"":window.location.search);return t||m.set(n.current,e),e}));return t.useInsertionEffect((()=>{const t=h.get(n.current)||[],r=()=>{u(m.get(n.current)||e)};return h.set(n.current,t.concat(r)),()=>{const t=h.get(n.current)||[];h.set(n.current,t.filter((t=>t!==r)))}}),[]),{state:a,getState:t.useCallback((()=>m.get(n.current)||n.current),[]),setState:t.useCallback((t=>{const e=m.get(n.current)||n.current;if("function"==typeof t){const r=t(e);if(o(e,r))return!1;m.set(n.current,r),(h.get(n.current)||[]).forEach((t=>{t()}))}else{if(o(e,t))return!1;m.set(n.current,t),(h.get(n.current)||[]).forEach((t=>{t()}))}}),[]),parse:c,stringify:s}}(r()?function(t,e){return{...e,...d(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}`,c=i("function"==typeof t?t(a()):a()),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,w as useUrlState};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "state-in-url",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
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": [
@@ -80,7 +80,7 @@
80
80
  "cleanup": "rm -rf dist",
81
81
  "setup": "playwright install --with-deps",
82
82
  "cleanupDist": "wireit",
83
- "prepack": "npm run test:unit && npm run build",
83
+ "prepack": "npm run test:unit && npm run build && npm run build:demo",
84
84
  "prepare": "npx husky"
85
85
  },
86
86
  "wireit": {