state-in-url 3.1.0 → 4.0.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
@@ -146,7 +146,7 @@ import { userState } from './userState';
146
146
  function MyComponent() {
147
147
  // can pass `replace` arg, it's control will `setUrl` will use `rounter.push` or `router.replace`, default replace=true
148
148
  // can pass `searchParams` from server components
149
- const { urlState, setUrl } = useUrlState({ defaultState: userState });
149
+ const { urlState, setUrl, setState } = useUrlState({ defaultState: userState });
150
150
 
151
151
  // won't let you to accidently mutate state directly, requires TS
152
152
  // urlState.name = 'John' // <- error
@@ -162,7 +162,9 @@ function MyComponent() {
162
162
 
163
163
  // same api as React.useState
164
164
  <input value={urlState.name}
165
- onChange={(ev) => { setUrl(curr => ({ ...curr, name: ev.target.value })) }}
165
+ onChange={(ev) => { setState(curr => ({ ...curr, name: ev.target.value })) }}
166
+ // Can update state immediately but sync change to url as needed
167
+ onBlur={() => setUrl()}
166
168
  />
167
169
 
168
170
  <button onClick={() => setUrl(userState)}>
@@ -183,14 +185,14 @@ function MyComponent() {
183
185
  export const form: Form = {
184
186
  name: '',
185
187
  age: undefined,
186
- 'agree to terms': false,
188
+ agree_to_terms: false,
187
189
  tags: [],
188
190
  };
189
191
 
190
192
  type Form = {
191
193
  name: string;
192
194
  age?: number;
193
- 'agree to terms': boolean;
195
+ agree_to_terms: boolean;
194
196
  tags: { id: string; value: { text: string; time: Date } }[];
195
197
  };
196
198
  ```
@@ -389,14 +391,14 @@ API is same as for Next.js version, except can pass options from [NavigateOption
389
391
  export const form: Form = {
390
392
  name: '',
391
393
  age: undefined,
392
- 'agree to terms': false,
394
+ agree_to_terms: false,
393
395
  tags: [],
394
396
  };
395
397
 
396
398
  type Form = {
397
399
  name: string;
398
400
  age?: number;
399
- 'agree to terms': boolean;
401
+ agree_to_terms: boolean;
400
402
  tags: { id: string; value: { text: string; time: Date } }[];
401
403
  };
402
404
 
@@ -408,7 +410,7 @@ import { useUrlState } from 'state-in-url/react-router';
408
410
  import { form } from './form';
409
411
 
410
412
  function TagsComponent() {
411
- const { urlState, setUrl } = useUrlState({ defaultState: form });
413
+ const { urlState, setUrl, setState } = useUrlState({ defaultState: form });
412
414
 
413
415
  const onChangeTags = React.useCallback(
414
416
  (tag: (typeof tags)[number]) => {
@@ -436,6 +438,12 @@ function TagsComponent() {
436
438
  ))}
437
439
  </div>
438
440
  </Field>
441
+
442
+ <input value={urlState.name}
443
+ onChange={(ev) => { setState(curr => ({ ...curr, name: ev.target.value })) }}
444
+ // Can update state immediately but sync change to url as needed
445
+ onBlur={() => setUrl()}
446
+ />
439
447
  </div>
440
448
  );
441
449
  }
@@ -5,6 +5,7 @@ import { type DeepReadonly, type JSONCompatible } from "../../utils";
5
5
  *
6
6
  * @param {JSONCompatible<T>} [defaultState] Fallback (default) values for state
7
7
  * @param {?SearchParams<T>} [searchParams] searchParams from Next server component
8
+ * @param {boolean} [useHistory] use window.history for navigation, no _rsc requests https://github.com/vercel/next.js/discussions/59167
8
9
  *
9
10
  * * Example:
10
11
  * ```ts
@@ -22,11 +23,12 @@ import { type DeepReadonly, type JSONCompatible } from "../../utils";
22
23
  *
23
24
  * * Docs {@link https://github.com/asmyshlyaev177/state-in-url/tree/master/packages/urlstate/next/useUrlState#api}
24
25
  */
25
- export declare function useUrlState<T extends JSONCompatible>({ defaultState, searchParams, ...opts }: {
26
+ export declare function useUrlState<T extends JSONCompatible>({ defaultState, searchParams, useHistory, ...opts }: {
26
27
  defaultState: T;
27
28
  searchParams?: object;
28
29
  replace?: boolean;
29
30
  scroll?: boolean;
31
+ useHistory?: boolean;
30
32
  }): {
31
33
  /**
32
34
  * * Example:
@@ -1 +1 @@
1
- import{useRouter as t,useSearchParams as e}from"next/navigation";import a from"react";import{parseSPObj as r}from"../../parseSPObj.mjs";import{useUrlStateBase as s}from"../../useUrlStateBase/useUrlStateBase.mjs";import{isSSR as o,filterUnknownParams as m,filterUnknownParamsClient as l}from"../../utils.mjs";function p({defaultState:p,searchParams:c,...i}){const n=t(),{state:f,updateState:S,updateUrl:d,getState:j}=s(p,n,(({parse:t})=>o()?r(m(p,c),p):t(l(p)))),U=a.useCallback(((t,e)=>{const a={...u,...i,...e};d(t,a)}),[d,i]),b=e();return a.useEffect((()=>{S(m(p,r(Object.fromEntries([...b.entries()]),p)))}),[b]),{setState:S,updateState:S,setUrl:U,updateUrl:U,urlState:f,state:f,getState:j}}const u={replace:!0,scroll:!1};export{p as useUrlState};
1
+ import{useRouter as t,useSearchParams as e}from"next/navigation";import r from"react";import{parseSPObj as a}from"../../parseSPObj.mjs";import{useUrlStateBase as s}from"../../useUrlStateBase/useUrlStateBase.mjs";import{routerHistory as o,isSSR as m,filterUnknownParams as u,filterUnknownParamsClient as i}from"../../utils.mjs";function l({defaultState:l,searchParams:c,useHistory:n,...f}){const S=void 0===n||n?o:t(),{state:d,updateState:j,updateUrl:U,getState:b}=s(l,S,(({parse:t})=>m()?a(u(l,c),l):t(i(l)))),g=r.useCallback(((t,e)=>{const r={...p,...f,...e};U(t,r)}),[U,f]),v=e();return r.useEffect((()=>{j(u(l,a(Object.fromEntries([...v.entries()]),l)))}),[v]),{setState:j,updateState:j,setUrl:g,updateUrl:g,urlState:d,state:d,getState:b}}const p={replace:!0,scroll:!1};export{l as useUrlState};
@@ -5,6 +5,7 @@ import { type DeepReadonly, type JSONCompatible } from "../../utils";
5
5
  *
6
6
  * @param {JSONCompatible<T>} [defaultState] Fallback (default) values for state
7
7
  * @param {NavigateOptions} [NavigateOptions] See type from `react-router-dom`
8
+ * @param {boolean} [useHistory] use window.history for navigation
8
9
  * * Example:
9
10
  * ```ts
10
11
  * export const form = { name: '', age: 0 };
@@ -18,17 +19,18 @@ import { type DeepReadonly, type JSONCompatible } from "../../utils";
18
19
  *
19
20
  * * Docs {@link https://github.com/asmyshlyaev177/state-in-url/tree/master/packages/urlstate/react-router/useUrlState#api}
20
21
  */
21
- export declare function useUrlState<T extends JSONCompatible>({ defaultState, ...initOpts }: {
22
+ export declare function useUrlState<T extends JSONCompatible>({ defaultState, useHistory, ...initOpts }: {
22
23
  defaultState: T;
24
+ useHistory?: boolean;
23
25
  } & NavigateOptions): {
24
26
  /**
25
27
  * * Example:
26
28
  * ```ts
27
- * setUrlState({ name: 'test' });
29
+ * setState({ name: 'test' });
28
30
  * // or
29
- * setUrlState(curr => ({ ...curr, name: 'test' }) );
31
+ * setState(curr => ({ ...curr, name: 'test' }) );
30
32
  * // can pass optional React-Router `NavigateOptions`
31
- * setUrlState(curr => ({ ...curr, name: 'test', preventScrollReset: false }) );
33
+ * setState(curr => ({ ...curr, name: 'test', preventScrollReset: false }) );
32
34
  * ```
33
35
  *
34
36
  * * Docs {@link https://github.com/asmyshlyaev177/state-in-url/tree/master/packages/urlstate/react-router/useUrlState#updatestate}
@@ -1 +1 @@
1
- import t from"react";import{useNavigate as e,useSearchParams as r}from"react-router-dom";import{parseSPObj as a}from"../../parseSPObj.mjs";import{useUrlStateBase as s}from"../../useUrlStateBase/useUrlStateBase.mjs";import{filterUnknownParamsClient as o,assignValue as m,filterUnknownParams as p}from"../../utils.mjs";function u({defaultState:u,...c}){const S=e(),f=t.useMemo((()=>({replace:(t,e)=>S(t,{...l,...c,...e}),push:(t,e)=>S(t,{...l,...c,...e})})),[S,c]),{state:i,updateState:n,updateUrl:d,getState:j}=s(u,f,(({parse:t})=>t(o(u)))),U=t.useCallback(((t,e)=>{const r={...l,...c,...e};d(t,r)}),[c]),[b]=r();return t.useEffect((()=>{n(m(u,i,p(u,a(Object.fromEntries([...b.entries()]),u))))}),[b]),{setState:n,updateState:n,setUrl:U,updateUrl:U,urlState:i,state:i,getState:j}}const l={replace:!0,preventScrollReset:!0};export{u as useUrlState};
1
+ import t from"react";import{useNavigate as e,useSearchParams as r}from"react-router-dom";import{parseSPObj as a}from"../../parseSPObj.mjs";import{useUrlStateBase as s}from"../../useUrlStateBase/useUrlStateBase.mjs";import{routerHistory as o,filterUnknownParamsClient as u,assignValue as m,filterUnknownParams as p}from"../../utils.mjs";function l({defaultState:l,useHistory:S,...f}){const i=e(),n=t.useMemo((()=>S?o:{replace:(t,e)=>i(t,{...c,...f,...e}),push:(t,e)=>i(t,{...c,...f,...e})}),[i,f]),{state:d,updateState:j,updateUrl:U,getState:b}=s(l,n,(({parse:t})=>t(u(l)))),g=t.useCallback(((t,e)=>{const r={...c,...f,...e};U(t,r)}),[f]),[B]=r();return t.useEffect((()=>{j(m(l,d,p(l,a(Object.fromEntries([...B.entries()]),l))))}),[B]),{setState:j,updateState:j,setUrl:g,updateUrl:g,urlState:d,state:d,getState:b}}const c={replace:!0,preventScrollReset:!0};export{l as useUrlState};
@@ -1,5 +1,5 @@
1
1
  import { useUrlEncode } from "../useUrlEncode";
2
- import { type DeepReadonly, type JSONCompatible } from "../utils";
2
+ import { type DeepReadonly, type JSONCompatible, type Router } from "../utils";
3
3
  /**
4
4
  * A custom React hook to create custom hooks.
5
5
  *
@@ -35,8 +35,4 @@ interface OptionsObject {
35
35
  export interface Options extends OptionsObject {
36
36
  replace?: boolean;
37
37
  }
38
- type Router = {
39
- push: (href: string, opts: object) => void;
40
- replace: (href: string, opts: object) => void;
41
- };
42
38
  export {};
@@ -1 +1 @@
1
- import t from"react";import{useInsertionEffect as e}from"../useInsertionEffect.mjs";import{useSharedState as n}from"../useSharedState/useSharedState.mjs";import{useUrlEncode as o}from"../useUrlEncode/useUrlEncode.mjs";import{filterUnknownParamsClient as r}from"../utils.mjs";function s(s,c,i){const{parse:u,stringify:f}=o(s),{state:h,getState:l,setState:m}=n(s,(()=>i?.({parse:u})||s));e((()=>{const t=()=>{const t=u(r(s));m(t)};return window.addEventListener(a,t),()=>{window.removeEventListener(a,t)}}),[m]);const p=t.useRef([]),d=t.useCallback(((t,e)=>{const n="function"==typeof t?t(l()):t?{...l(),...t}:l(),o=f(n,function(t){const e=Object.keys(t),n=window.location.search,o=new URLSearchParams(n),r=new URLSearchParams;return o.forEach(((t,n)=>!e.includes(n)&&r.set(n,t))),r}(s)),r=`${window.location.pathname}${o.length?"?":""}${o}${window.location.hash}`;if(r===`${window.location.pathname}${window.location.search}${window.location.hash}`)return;let a;m(n);const{replace:i,...u}=e||{};p.current.push([i?"replace":"push",r,u]),1===p.current.length&&queueMicrotask((()=>{for(;p.current.length;){const t=p.current.shift();if(!t)break;t?.[1]!==a?.[1]&&(a=t)}if(!a)return;const[t,e,n]=a;a=void 0,c[t](e,n)}))}),[c,f,l]);return{updateState:m,updateUrl:d,state:h,getState:l}}const a="popstate";export{s as useUrlStateBase};
1
+ import t from"react";import{useInsertionEffect as e}from"../useInsertionEffect.mjs";import{useSharedState as n}from"../useSharedState/useSharedState.mjs";import{useUrlEncode as o}from"../useUrlEncode/useUrlEncode.mjs";import{filterUnknownParamsClient as r}from"../utils.mjs";function s(s,c,i){const{parse:u,stringify:h}=o(s),{state:l,getState:m,setState:p}=n(s,(()=>i?.({parse:u})||s));e((()=>{const t=()=>{const t=u(r(s));p(t)};return window.addEventListener(a,t),()=>{window.removeEventListener(a,t)}}),[p]);const d=t.useRef([]),w=t.useCallback(((t,e)=>{const n="function"==typeof t?t(m()):t?{...m(),...t}:m(),o=h(n,function(t){const e=Object.keys(t),n=window.location.search,o=new URLSearchParams(n),r=new URLSearchParams;return o.forEach(((t,n)=>!e.includes(n)&&r.set(n,t))),r}(s)),r=`${window.location.pathname}${o.length?"?":""}${o}${window.location.hash}`;if(r===`${window.location.pathname}${window.location.search}${window.location.hash}`)return;let a;p(n);const{replace:i,...u}=e||{};d.current.push([i?"replace":"push",r,u]),1===d.current.length&&queueMicrotask((()=>{for(;d.current.length;){const t=d.current.shift();t&&t?.[1]!==a?.[1]&&(a=t)}const[t,e,n]=a||{};a=void 0,t&&c[t](e,n)}))}),[c,h,m]);return{updateState:p,updateUrl:w,state:l,getState:m}}const a="popstate";export{s as useUrlStateBase};
package/dist/utils.d.ts CHANGED
@@ -26,3 +26,8 @@ export declare const isEqual: (val1: unknown, val2: unknown) => boolean;
26
26
  export declare function filterUnknownParamsClient<T extends object>(shape: T): string;
27
27
  export declare function filterUnknownParams<T extends object>(shape: T, searchParams?: object): T;
28
28
  export declare function assignValue<T extends object>(shape: T, curr: Partial<T>, newVal: Partial<T>): T;
29
+ export interface Router {
30
+ push: (href: string, opts: object) => void;
31
+ replace: (href: string, opts: object) => void;
32
+ }
33
+ export declare const routerHistory: Router;
package/dist/utils.mjs CHANGED
@@ -1 +1 @@
1
- const t=t=>{const n=typeof t,e=null===t,r=Array.isArray(t),c=t instanceof Date;return(e?"null":c&&"date")||r&&"array"||!e&&!c&&!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||"",c=(t,n)=>JSON.stringify(t)===JSON.stringify(n);function o(t){const n=new URLSearchParams;return s(t,[...new URLSearchParams(window.location.search).entries()]).forEach((([t,e])=>n.set(t,e))),n.toString()}function i(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.replaceAll("+"," "),n]))}function a(t,n,e){const r=Object.assign({},t,n);return Object.entries(t).forEach((([n])=>{const c=n,o=void 0!==e[c];r[c]=o?e[c]:t[c]})),r}export{a as assignValue,i as filterUnknownParams,o as filterUnknownParamsClient,e as getParams,c as isEqual,n as isSSR,t as typeOf};
1
+ const t=t=>{const n=typeof t,e=null===t,r=Array.isArray(t),o=t instanceof Date;return(e?"null":o&&"date")||r&&"array"||!e&&!o&&!r&&"object"===n&&"object"||n},n=()=>"undefined"==typeof window,e=t=>new URLSearchParams("string"==typeof t?r(t):t?.toString?.()||""),r=t=>t.split("?")?.[1]||t||"",o=(t,n)=>JSON.stringify(t)===JSON.stringify(n);function i(t){const n=new URLSearchParams;return 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.replaceAll("+"," "),n]))}function a(t,n,e){const r=Object.assign({},t,n);return Object.entries(t).forEach((([n])=>{const o=n,i=void 0!==e[o];r[o]=i?e[o]:t[o]})),r}const l={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,l as routerHistory,t as typeOf};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "state-in-url",
3
- "version": "3.1.0",
3
+ "version": "4.0.1",
4
4
  "description": "Easily share complex state objects between unrelated React components, preserve types and structure, with TS validation. Deep links and url state synchronization wthout any hasssle or boilerplate.",
5
5
  "homepage": "https://state-in-url.dev",
6
6
  "keywords": [