state-in-url 4.3.3 → 5.1.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.
Files changed (40) hide show
  1. package/README.md +89 -1
  2. package/dist/constants/constants.js +1 -0
  3. package/dist/constants/index.js +1 -0
  4. package/dist/encodeState/encodeState.js +1 -0
  5. package/dist/encodeState/index.js +1 -0
  6. package/dist/encoder/encoder.js +1 -0
  7. package/dist/encoder/index.js +1 -0
  8. package/dist/index.js +1 -0
  9. package/dist/next/index.js +1 -0
  10. package/dist/next/useUrlState/index.js +1 -0
  11. package/dist/next/useUrlState/useUrlState.d.ts +0 -30
  12. package/dist/next/useUrlState/useUrlState.js +1 -0
  13. package/dist/next/useUrlState/useUrlState.mjs +1 -1
  14. package/dist/parseSPObj.js +1 -0
  15. package/dist/react-router/index.js +1 -0
  16. package/dist/react-router/useUrlState/index.js +1 -0
  17. package/dist/react-router/useUrlState/useUrlState.d.ts +0 -26
  18. package/dist/react-router/useUrlState/useUrlState.js +1 -0
  19. package/dist/react-router/useUrlState/useUrlState.mjs +1 -1
  20. package/dist/remix/index.d.ts +1 -0
  21. package/dist/remix/index.js +1 -0
  22. package/dist/remix/index.mjs +1 -0
  23. package/dist/remix/useUrlState/index.d.ts +1 -0
  24. package/dist/remix/useUrlState/index.js +1 -0
  25. package/dist/remix/useUrlState/index.mjs +1 -0
  26. package/dist/remix/useUrlState/useUrlState.d.ts +82 -0
  27. package/dist/remix/useUrlState/useUrlState.js +1 -0
  28. package/dist/remix/useUrlState/useUrlState.mjs +1 -0
  29. package/dist/subscribers.js +1 -0
  30. package/dist/useInsertionEffect.js +1 -0
  31. package/dist/useSharedState/index.js +1 -0
  32. package/dist/useSharedState/useSharedState.js +1 -0
  33. package/dist/useUrlEncode/index.js +1 -0
  34. package/dist/useUrlEncode/useUrlEncode.js +1 -0
  35. package/dist/useUrlEncode/useUrlEncode.mjs +1 -1
  36. package/dist/useUrlStateBase/index.js +1 -0
  37. package/dist/useUrlStateBase/useUrlStateBase.js +1 -0
  38. package/dist/utils.d.ts +1 -1
  39. package/dist/utils.js +1 -0
  40. package/package.json +63 -17
package/README.md CHANGED
@@ -102,6 +102,7 @@ This library is a good alternative for NUQS.
102
102
  - [Installation](#installation)
103
103
  - [useUrlState hook](#useurlstate)
104
104
  - - [Next.js](#useurlstate-hook-for-nextjs)
105
+ - - [Remix](#useurlstate-hook-for-remixjs)
105
106
  - - [React-Router](#useurlstate-hook-for-react-router)
106
107
  - [Other helpers](#other-hooks-and-helpers)
107
108
  - - [`useUrlStateBase` for other routers](#useurlstatebase-hook-for-others-routers)
@@ -454,6 +455,93 @@ function SettingsComponent() {
454
455
 
455
456
  </details>
456
457
 
458
+ ### useUrlState hook for Remix.js
459
+
460
+ API is same as for Next.js version, except can pass options from [NavigateOptions](https://github.com/remix-run/react-router/blob/bc693ed9f39170bda13b9e1b282fb8e9d5925f66/packages/react-router/lib/context.ts#L99) type.
461
+
462
+ [API Docs](packages/urlstate/remix/useUrlState)
463
+
464
+ #### Example
465
+
466
+ ```typescript
467
+ export const form: Form = {
468
+ name: '',
469
+ age: undefined,
470
+ agree_to_terms: false,
471
+ tags: [],
472
+ };
473
+
474
+ type Form = {
475
+ name: string;
476
+ age?: number;
477
+ agree_to_terms: boolean;
478
+ tags: { id: string; value: { text: string; time: Date } }[];
479
+ };
480
+
481
+ ```
482
+
483
+ ```typescript
484
+ import { useUrlState } from 'state-in-url/remix';
485
+
486
+ import { form } from './form';
487
+
488
+ function TagsComponent() {
489
+ const { urlState, setUrl, setState } = useUrlState(form);
490
+
491
+ const onChangeTags = React.useCallback(
492
+ (tag: (typeof tags)[number]) => {
493
+ setUrl((curr) => ({
494
+ ...curr,
495
+ tags: curr.tags.find((t) => t.id === tag.id)
496
+ ? curr.tags.filter((t) => t.id !== tag.id)
497
+ : curr.tags.concat(tag),
498
+ }));
499
+ },
500
+ [setUrl],
501
+ );
502
+
503
+ return (
504
+ <div>
505
+ <Field text="Tags">
506
+ <div className="flex flex-wrap gap-2">
507
+ {tags.map((tag) => (
508
+ <Tag
509
+ active={!!urlState.tags.find((t) => t.id === tag.id)}
510
+ text={tag.value.text}
511
+ onClick={() => onChangeTags(tag)}
512
+ key={tag.id}
513
+ />
514
+ ))}
515
+ </div>
516
+ </Field>
517
+
518
+ <input value={urlState.name}
519
+ onChange={(ev) => { setState(curr => ({ ...curr, name: ev.target.value })) }}
520
+ // Can update state immediately but sync change to url as needed
521
+ onBlur={() => setUrl()}
522
+ />
523
+ </div>
524
+ );
525
+ }
526
+
527
+ const tags = [
528
+ {
529
+ id: '1',
530
+ value: { text: 'React.js', time: new Date('2024-07-17T04:53:17.000Z') },
531
+ },
532
+ {
533
+ id: '2',
534
+ value: { text: 'Next.js', time: new Date('2024-07-18T04:53:17.000Z') },
535
+ },
536
+ {
537
+ id: '3',
538
+ value: { text: 'TailwindCSS', time: new Date('2024-07-19T04:53:17.000Z') },
539
+ },
540
+ ];
541
+ ```
542
+
543
+ [Example code](packages/example-remix2/app/routes/Form-for-test.tsx)
544
+
457
545
  ### useUrlState hook for React-Router
458
546
 
459
547
  API is same as for Next.js version, except can pass options from [NavigateOptions](https://github.com/remix-run/react-router/blob/bc693ed9f39170bda13b9e1b282fb8e9d5925f66/packages/react-router/lib/context.ts#L99) type.
@@ -629,7 +717,7 @@ See [Contributing doc](CONTRIBUTING.md)
629
717
 
630
718
  - [x] hook for `Next.js`
631
719
  - [x] hook for `react-router`
632
- - [ ] hook for `remix`
720
+ - [x] hook for `remix`
633
721
  - [ ] hook for `svelte`
634
722
  - [ ] hook for `astro`
635
723
  - [ ] hook for store state in hash ?
@@ -0,0 +1 @@
1
+ "use strict";var e=require("../utils.js");const i={date:"\u23F2",undefined:"\u2219undefined"};const u=e.isSafari?330:70;exports.SYMBOLS=i;exports.TIMEOUT=u;
@@ -0,0 +1 @@
1
+ "use strict";var r=require("./constants.js");exports.SYMBOLS=r.SYMBOLS;exports.TIMEOUT=r.TIMEOUT;
@@ -0,0 +1 @@
1
+ "use strict";var i=require("../encoder/encoder.js");var s=require("../utils.js");function d(o,t,r){const n=s.getParams(r);for(const[e,c]of Object.entries(o||{})){const a=t?.[e];if(JSON.stringify(c)!==JSON.stringify(a)){n.set(e,i.encode(c))}}return n.toString()}function u(o,t){return{...t||{},...Object.fromEntries([...s.getParams(o).entries()].map(([r,n])=>{const e=t?.[r];return[r,i.decode(n,e)??e]}))}}exports.decodeState=u;exports.encodeState=d;
@@ -0,0 +1 @@
1
+ "use strict";var e=require("./encodeState.js");exports.decodeState=e.decodeState;exports.encodeState=e.encodeState;
@@ -0,0 +1 @@
1
+ "use strict";var n=require("../constants/constants.js");var o=require("../utils.js");function S(e){if(typeof e==="function"||typeof e==="symbol")return"";if(v(e)){return e}return JSON.stringify(structuredClone(e),c).replace(/'/g,"%27").replace(/"/g,"'")}function c(e,r){const i=o.typeOf(r);if(i!=="object"&&i!=="array"){return s(r)}if(i==="object"){const t=r;for(const u of Object.keys(t)){t[u]=c(e,t[u])}return t}if(i==="array"){return r.map(t=>c(e,t))}return r}const s=e=>{switch(o.typeOf(e)){case"date":return n.SYMBOLS.date+new Date(e).toISOString();case"undefined":return n.SYMBOLS.undefined;default:return e}};function p(e,r){return d(e.replace(/'/g,'"').replace(/%27/g,"'"),r)}function d(e,r){try{return JSON.parse(e,a)}catch{return r}}function a(e,r){return typeof r==="string"?f(r):r}const f=e=>{if(e===n.SYMBOLS.undefined)return void 0;if(e?.startsWith?.(n.SYMBOLS.date))return new Date(e.slice(1));return e};const O=new RegExp(`^(${n.SYMBOLS.undefined}|${n.SYMBOLS.date})`);const v=e=>O.test(String(e));exports.decode=p;exports.decodePrimitive=f;exports.encode=S;exports.encodePrimitive=s;exports.parseJSON=d;exports.reviver=a;
@@ -0,0 +1 @@
1
+ "use strict";var e=require("./encoder.js");exports.decode=e.decode;exports.encode=e.encode;
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ "use strict";var e=require("./encoder/encoder.js");var r=require("./encodeState/encodeState.js");var a=require("./next/useUrlState/useUrlState.js");var u=require("./useSharedState/useSharedState.js");var S=require("./useUrlEncode/useUrlEncode.js");var d=require("./useUrlStateBase/useUrlStateBase.js");var t=require("./utils.js");exports.decode=e.decode;exports.encode=e.encode;exports.decodeState=r.decodeState;exports.encodeState=r.encodeState;exports.useUrlState=a.useUrlState;exports.useSharedState=u.useSharedState;exports.useUrlEncode=S.useUrlEncode;exports.useUrlStateBase=d.useUrlStateBase;exports.isSSR=t.isSSR;exports.typeOf=t.typeOf;
@@ -0,0 +1 @@
1
+ "use strict";var e=require("./useUrlState/useUrlState.js");exports.useUrlState=e.useUrlState;
@@ -0,0 +1 @@
1
+ "use strict";var e=require("./useUrlState.js");exports.useUrlState=e.useUrlState;
@@ -1,28 +1,5 @@
1
1
  import { useRouter } from "next/navigation";
2
2
  import { type JSONCompatible } from "../../utils";
3
- /**
4
- * @deprecated .
5
- * * use format `useUrlState(defaultState, { ...otherParams })`
6
- *
7
- * .
8
- */
9
- export declare function useUrlState<T extends JSONCompatible>({ defaultState: T, searchParams, replace, scroll, useHistory, }: OldParams<T>): {
10
- /**
11
- * @deprecated use `urlState`
12
- */
13
- state: T;
14
- urlState: T;
15
- /**
16
- * @deprecated use `setState`
17
- */
18
- updateState: (value: Partial<T> | ((currState: T) => T)) => void;
19
- setState: (value: Partial<T> | ((currState: T) => T)) => void;
20
- /**
21
- * @deprecated use `setUrl`
22
- */
23
- updateUrl: (value?: Partial<T> | ((currState: T) => T)) => void;
24
- setUrl: (value?: Partial<T> | ((currState: T) => T)) => void;
25
- };
26
3
  /**
27
4
  * NextJS hook. Returns `urlState`, `setState`, and `setUrl` functions
28
5
  *
@@ -103,13 +80,6 @@ type RouterOptions = NonNullable<Parameters<Router["push"]>[1] | Parameters<Rout
103
80
  interface Options extends RouterOptions {
104
81
  replace?: boolean;
105
82
  }
106
- interface OldParams<T> {
107
- defaultState: T;
108
- searchParams?: object;
109
- replace?: boolean;
110
- scroll?: boolean;
111
- useHistory?: boolean;
112
- }
113
83
  type Params = {
114
84
  searchParams?: object;
115
85
  replace?: boolean;
@@ -0,0 +1 @@
1
+ "use strict";var h=require("react");var p=require("next/navigation");var S=require("../../parseSPObj.js");var k=require("../../useUrlStateBase/useUrlStateBase.js");var r=require("../../utils.js");function q(e){return e&&e.__esModule?e:{default:e}}var s=q(h);function _(e,a){const u=a?.useHistory;const l=p.useRouter();const d=s.default.useMemo(()=>({push:(...t)=>{if(u===void 0?true:!!u){r.routerHistory.push(...t)}else{l.push(...t)}},replace:(...t)=>{if(u===void 0?true:!!u){r.routerHistory.replace(...t)}else{l.replace(...t)}}}),[l]);const{state:P,updateState:o,updateUrl:n,reset:c,getState:U}=k.useUrlStateBase(e,d,({parse:t})=>{return r.isSSR?S.parseSPObj(r.filterUnknownParams(e,a?.searchParams),e):t(r.filterUnknownParamsClient(e,a?.searchParams))});const i=s.default.useMemo(()=>({...j,scroll:a?.scroll,replace:a?.replace}),[]);const m=s.default.useCallback((t,b)=>n(t,{...i,...b}),[n]);const f=p.useSearchParams();s.default.useLayoutEffect(()=>{o(r.filterUnknownParams(e,S.parseSPObj(Object.fromEntries([...f.entries()]),e)))},[f]);const v=s.default.useCallback(t=>{c({...i,...t})},[c]);return{setState:o,setUrl:m,urlState:P,reset:v,getState:U}}const j={replace:true,scroll:false};exports.useUrlState=_;
@@ -1 +1 @@
1
- import a from"react";import{useRouter as C,useSearchParams as H}from"next/navigation";import{parseSPObj as d}from"../../parseSPObj.mjs";import{useUrlStateBase as g}from"../../useUrlStateBase/useUrlStateBase.mjs";import{routerHistory as U,filterUnknownParams as h,isSSR as v,filterUnknownParamsClient as w}from"../../utils.mjs";function E(t,r){const s="defaultState"in t?t.defaultState:t;const u="defaultState"in t?t.searchParams:r?.searchParams;const l="defaultState"in t?t.useHistory:r?.useHistory;const P="defaultState"in t?{scroll:t.scroll,replace:t.replace}:{scroll:r?.scroll,replace:r?.replace};const o=C();const b=a.useMemo(()=>({push:(...e)=>{if(l===void 0?true:!!l){U.push(...e)}else{o.push(...e)}},replace:(...e)=>{if(l===void 0?true:!!l){U.replace(...e)}else{o.replace(...e)}}}),[o]);const{state:n,updateState:c,updateUrl:i,reset:p,getState:k}=g(s,b,({parse:e})=>{return v?d(h(s,u),s):e(w(s,u))});const f=a.useMemo(()=>({...M,...P}),[]);const m=a.useCallback((e,j)=>i(e,{...f,...j}),[i]);const S=H();a.useLayoutEffect(()=>{c(h(s,d(Object.fromEntries([...S.entries()]),s)))},[S]);const y=a.useCallback(e=>{p({...f,...e})},[p]);return{setState:c,updateState:c,setUrl:m,updateUrl:m,urlState:n,state:n,reset:y,getState:k}}const M={replace:true,scroll:false};export{E as useUrlState};
1
+ import s from"react";import{useRouter as k,useSearchParams as C}from"next/navigation";import{parseSPObj as p}from"../../parseSPObj.mjs";import{useUrlStateBase as y}from"../../useUrlStateBase/useUrlStateBase.mjs";import{routerHistory as m,filterUnknownParams as f,isSSR as g,filterUnknownParamsClient as j}from"../../utils.mjs";function v(t,r){const a=r?.useHistory;const o=k();const S=s.useMemo(()=>({push:(...e)=>{if(a===void 0?true:!!a){m.push(...e)}else{o.push(...e)}},replace:(...e)=>{if(a===void 0?true:!!a){m.replace(...e)}else{o.replace(...e)}}}),[o]);const{state:h,updateState:l,updateUrl:c,reset:u,getState:P}=y(t,S,({parse:e})=>{return g?p(f(t,r?.searchParams),t):e(j(t,r?.searchParams))});const n=s.useMemo(()=>({...w,scroll:r?.scroll,replace:r?.replace}),[]);const U=s.useCallback((e,d)=>c(e,{...n,...d}),[c]);const i=C();s.useLayoutEffect(()=>{l(f(t,p(Object.fromEntries([...i.entries()]),t)))},[i]);const b=s.useCallback(e=>{u({...n,...e})},[u]);return{setState:l,setUrl:U,urlState:h,reset:b,getState:P}}const w={replace:true,scroll:false};export{v as useUrlState};
@@ -0,0 +1 @@
1
+ "use strict";var c=require("./encoder/encoder.js");function s(r,e){return{...e,...o(JSON.stringify(r),e)}}function o(r,e){try{return JSON.parse(r,u)}catch{return e}}const u=(r,e)=>{const t=typeof e==="string";const n=t&&c.decode(e?.replace?.(/'/g,'"'));return r&&t?n:e};exports.parseSPObj=s;
@@ -0,0 +1 @@
1
+ "use strict";var e=require("./useUrlState/useUrlState.js");exports.useUrlState=e.useUrlState;
@@ -0,0 +1 @@
1
+ "use strict";var e=require("./useUrlState.js");exports.useUrlState=e.useUrlState;
@@ -1,25 +1,5 @@
1
1
  import { type NavigateOptions } from "react-router-dom";
2
2
  import { type JSONCompatible } from "../../utils";
3
- /**
4
- * @deprecated .
5
- * * use format `useUrlState(defaultState, { ...otherParams })`
6
- *
7
- * .
8
- */
9
- export declare function useUrlState<T extends JSONCompatible>({ defaultState: T, searchParams, replace, useHistory, }: OldParams<T>): {
10
- /**
11
- * @deprecated use `urlState`
12
- */
13
- state: T;
14
- urlState: T;
15
- /**
16
- * @deprecated use `setState`
17
- */
18
- updateState: (value: Partial<T> | ((currState: T) => T)) => void;
19
- setState: (value: Partial<T> | ((currState: T) => T)) => void;
20
- updateUrl: (value?: Partial<T> | ((currState: T) => T)) => void;
21
- setUrl: (value?: Partial<T> | ((currState: T) => T)) => void;
22
- };
23
3
  /**
24
4
  * React-router hook. Returns `urlState`, `setState`, and `setUrl` functions
25
5
  *
@@ -94,12 +74,6 @@ export declare function useUrlState<T extends JSONCompatible>(defaultState: T, p
94
74
  [key: string]: unknown;
95
75
  }) => void;
96
76
  };
97
- type OldParams<T> = {
98
- defaultState: T;
99
- useHistory?: boolean;
100
- searchParams?: object;
101
- replace?: boolean;
102
- } & NavigateOptions;
103
77
  interface Params extends NavigateOptions {
104
78
  useHistory?: boolean;
105
79
  searchParams?: object;
@@ -0,0 +1 @@
1
+ "use strict";var P=require("react");var c=require("react-router-dom");var _=require("../../parseSPObj.js");var g=require("../../useUrlStateBase/useUrlStateBase.js");var u=require("../../utils.js");function k(e){return e&&e.__esModule?e:{default:e}}var s=k(P);function R(e,l){const p=l?.useHistory;const a=s.default.useMemo(()=>({...h,replace:l?.replace,preventScrollReset:l?.preventScrollReset}),[]);const n=c.useNavigate();const v=s.default.useMemo(()=>p?u.routerHistory:{replace:(t,r)=>n(t,{...a,...r}),push:(t,r)=>n(t,{...a,...r})},[n]);const[o]=c.useSearchParams();const d=c.useHref("/");const{state:U,updateState:i,updateUrl:f,getState:m,reset:S}=g.useUrlStateBase(e,v,({parse:t})=>t(u.filterUnknownParamsClient(e,o.entries())),d);const b=s.default.useCallback((t,r)=>f(t,{...a,...r}),[f]);s.default.useEffect(()=>{i(u.assignValue(e,u.filterUnknownParams(e,_.parseSPObj(Object.fromEntries([...o.entries()]),e))))},[o]);const q=s.default.useCallback(t=>{S({...a,...t})},[S]);return{setState:i,setUrl:b,urlState:U,reset:q,getState:m}}const h={replace:true,preventScrollReset:true};exports.useUrlState=R;
@@ -1 +1 @@
1
- import a from"react";import{useNavigate as H,useSearchParams as g,useHref as k}from"react-router-dom";import{parseSPObj as P}from"../../parseSPObj.mjs";import{useUrlStateBase as h}from"../../useUrlStateBase/useUrlStateBase.mjs";import{routerHistory as y,filterUnknownParamsClient as C,assignValue as j,filterUnknownParams as w}from"../../utils.mjs";function E(t,l){const r="defaultState"in t?t.defaultState:t;const m="defaultState"in t?t.useHistory:l?.useHistory;const d="defaultState"in t?{replace:t.replace,preventScrollReset:t.preventScrollReset}:{replace:l?.replace,preventScrollReset:l?.preventScrollReset};const o=a.useMemo(()=>({...M,...d}),[]);const n=H();const U=a.useMemo(()=>m?y:{replace:(e,s)=>n(e,{...o,...s}),push:(e,s)=>n(e,{...o,...s})},[n]);const[c]=g();const v=k("/");const{state:p,updateState:u,updateUrl:i,getState:R,reset:S}=h(r,U,({parse:e})=>e(C(r,c.entries())),v);const f=a.useCallback((e,s)=>i(e,{...o,...s}),[i]);a.useEffect(()=>{u(j(r,w(r,P(Object.fromEntries([...c.entries()]),r))))},[c]);const b=a.useCallback(e=>{S({...o,...e})},[S]);return{setState:u,updateState:u,setUrl:f,updateUrl:f,urlState:p,state:p,reset:b,getState:R}}const M={replace:true,preventScrollReset:true};export{E as useUrlState};
1
+ import s from"react";import{useNavigate as g,useSearchParams as k,useHref as P}from"react-router-dom";import{parseSPObj as h}from"../../parseSPObj.mjs";import{useUrlStateBase as C}from"../../useUrlStateBase/useUrlStateBase.mjs";import{routerHistory as H,filterUnknownParamsClient as R,assignValue as d,filterUnknownParams as j}from"../../utils.mjs";function w(t,a){const i=a?.useHistory;const o=s.useMemo(()=>({...y,replace:a?.replace,preventScrollReset:a?.preventScrollReset}),[]);const n=g();const m=s.useMemo(()=>i?H:{replace:(e,r)=>n(e,{...o,...r}),push:(e,r)=>n(e,{...o,...r})},[n]);const[c]=k();const f=P("/");const{state:S,updateState:l,updateUrl:u,getState:U,reset:p}=C(t,m,({parse:e})=>e(R(t,c.entries())),f);const b=s.useCallback((e,r)=>u(e,{...o,...r}),[u]);s.useEffect(()=>{l(d(t,j(t,h(Object.fromEntries([...c.entries()]),t))))},[c]);const v=s.useCallback(e=>{p({...o,...e})},[p]);return{setState:l,setUrl:b,urlState:S,reset:v,getState:U}}const y={replace:true,preventScrollReset:true};export{w as useUrlState};
@@ -0,0 +1 @@
1
+ export { useUrlState } from "./useUrlState";
@@ -0,0 +1 @@
1
+ "use strict";var e=require("./useUrlState/useUrlState.js");exports.useUrlState=e.useUrlState;
@@ -0,0 +1 @@
1
+ import{useUrlState as t}from"./useUrlState/useUrlState.mjs";export{t as useUrlState};
@@ -0,0 +1 @@
1
+ export { useUrlState } from "./useUrlState";
@@ -0,0 +1 @@
1
+ "use strict";var e=require("./useUrlState.js");exports.useUrlState=e.useUrlState;
@@ -0,0 +1 @@
1
+ import{useUrlState as t}from"./useUrlState.mjs";export{t as useUrlState};
@@ -0,0 +1,82 @@
1
+ import { type NavigateOptions } from "react-router-dom";
2
+ import { type JSONCompatible } from "../../utils";
3
+ /**
4
+ * Remix hook. Returns `urlState`, `setState`, and `setUrl` functions
5
+ *
6
+ * @param {JSONCompatible<T>} [defaultState] Fallback (default) values for state
7
+ * @param {Object} params - Object with other parameters
8
+ * @param {NavigateOptions} params.NavigateOptions See type from [`react-router-dom`](https://api.reactrouter.com/v7/interfaces/react_router.NavigateOptions.html)
9
+ * @param {boolean} params.replace replace URL or push, default `true`
10
+ * @param {boolean} params.useHistory use window.history for navigation, default `false`
11
+ * @param {boolean} params.preventScrollReset keep scroll position, default `true`
12
+ * @returns {Object} [result] State and callbacks
13
+ * @returns {Object} [result.state] - current state object
14
+ * @returns {Function} [result.setUrl] - function to update state and url
15
+ * @returns {Function} [result.setState] - function to update state only
16
+ * @returns {Function} [result.reset] - function to reset state and url to default
17
+ * * Example:
18
+ * ```ts
19
+ * export const form = { name: '', age: 0 };
20
+ * const { urlState, setState, setUrl } = useUrlState(form, { replace: false, preventScrollReset: false });
21
+ *
22
+ * setState({ name: 'test' });
23
+ * setUrl({ name: 'test' }, { replace: true });
24
+ * // similar to React.useState
25
+ * setUrl(curr => ({ ...curr, name: 'test' }), { replace: true });
26
+ * // reset state and url
27
+ * reset();
28
+ * reset({ replace: true });
29
+ * // same as setState(form) with setUrl(form)
30
+ * ```
31
+ *
32
+ * * Docs {@link https://github.com/asmyshlyaev177/state-in-url/tree/master/packages/urlstate/remix/useUrlState}
33
+ */
34
+ export declare function useUrlState<T extends JSONCompatible>(defaultState: T, params?: Params): {
35
+ /**
36
+ * State object. Don't mutate directly, use `setState` or `setUrl`
37
+ */
38
+ urlState: T;
39
+ /**
40
+ * * Example:
41
+ * ```ts
42
+ * setState({ name: 'test' });
43
+ * // or
44
+ * setState(curr => ({ ...curr, name: 'test' }) );
45
+ * ```
46
+ *
47
+ * * Docs {@link https://github.com/asmyshlyaev177/state-in-url/tree/master/packages/urlstate/remix/useUrlState#updatestate}
48
+ */
49
+ setState: (value: Partial<T> | ((currState: T) => T)) => void;
50
+ /**
51
+ * * Example:
52
+ * ```ts
53
+ * setUrl({ name: 'test' });
54
+ * // or
55
+ * setUrl(curr => ({ ...curr, name: 'test' }), { replace: true } );
56
+ * // can pass optional React-Router `NavigateOptions`
57
+ * setUrl(curr => ({ ...curr, name: 'test' }), { preventScrollReset: false } );
58
+ * ```
59
+ *
60
+ * * Docs {@link https://github.com/asmyshlyaev177/state-in-url/tree/master/packages/urlstate/remix/useUrlState#updateurl}
61
+ */
62
+ setUrl: (value?: Partial<T> | ((currState: T) => T), options?: Params) => void;
63
+ /**
64
+ * * Example:
65
+ * ```ts
66
+ * reset();
67
+ * // or
68
+ * reset({ replace: false, preventScrollReset: false })
69
+ * ```
70
+ *
71
+ * * Docs {@link https://github.com/asmyshlyaev177/state-in-url/tree/master/packages/urlstate/remix/useUrlState#reset}
72
+ */
73
+ reset: (options?: NavigateOptions & {
74
+ [key: string]: unknown;
75
+ }) => void;
76
+ };
77
+ interface Params extends NavigateOptions {
78
+ useHistory?: boolean;
79
+ searchParams?: object;
80
+ replace?: boolean;
81
+ }
82
+ export {};
@@ -0,0 +1 @@
1
+ "use strict";var P=require("react");var o=require("@remix-run/react");var _=require("../../parseSPObj.js");var g=require("../../useUrlStateBase/useUrlStateBase.js");var u=require("../../utils.js");function k(e){return e&&e.__esModule?e:{default:e}}var s=k(P);function h(e,l){const p=l?.useHistory;const a=s.default.useMemo(()=>({...j,replace:l?.replace,preventScrollReset:l?.preventScrollReset}),[]);const n=o.useNavigate();const v=s.default.useMemo(()=>p?u.routerHistory:{replace:(t,r)=>n(t,{...a,...r}),push:(t,r)=>n(t,{...a,...r})},[n]);const[c]=o.useSearchParams();const d=o.useHref("/");const{state:U,updateState:i,updateUrl:f,getState:m,reset:S}=g.useUrlStateBase(e,v,({parse:t})=>t(u.filterUnknownParamsClient(e,c.entries())),d);const b=s.default.useCallback((t,r)=>f(t,{...a,...r}),[f]);s.default.useEffect(()=>{i(u.assignValue(e,u.filterUnknownParams(e,_.parseSPObj(Object.fromEntries([...c.entries()]),e))))},[c]);const q=s.default.useCallback(t=>{S({...a,...t})},[S]);return{setState:i,setUrl:b,urlState:U,reset:q,getState:m}}const j={replace:true,preventScrollReset:true};exports.useUrlState=h;
@@ -0,0 +1 @@
1
+ import s from"react";import{useNavigate as g,useSearchParams as k,useHref as P}from"@remix-run/react";import{parseSPObj as h}from"../../parseSPObj.mjs";import{useUrlStateBase as C}from"../../useUrlStateBase/useUrlStateBase.mjs";import{routerHistory as H,filterUnknownParamsClient as R,assignValue as d,filterUnknownParams as j}from"../../utils.mjs";function w(t,a){const i=a?.useHistory;const o=s.useMemo(()=>({...y,replace:a?.replace,preventScrollReset:a?.preventScrollReset}),[]);const n=g();const m=s.useMemo(()=>i?H:{replace:(e,r)=>n(e,{...o,...r}),push:(e,r)=>n(e,{...o,...r})},[n]);const[c]=k();const f=P("/");const{state:S,updateState:l,updateUrl:u,getState:U,reset:p}=C(t,m,({parse:e})=>e(R(t,c.entries())),f);const b=s.useCallback((e,r)=>u(e,{...o,...r}),[u]);s.useEffect(()=>{l(d(t,j(t,h(Object.fromEntries([...c.entries()]),t))))},[c]);const v=s.useCallback(e=>{p({...o,...e})},[p]);return{setState:l,setUrl:b,urlState:S,reset:v,getState:U}}const y={replace:true,preventScrollReset:true};export{w as useUrlState};
@@ -0,0 +1 @@
1
+ "use strict";const r=new WeakMap;const s=new WeakMap;const a={get(t){return s.get(t)||[]},add(t,e){const n=this.get(t);s.set(t,n.concat(e));return()=>this.remove(t,e)},remove(t,e){const n=this.get(t);const c=n.filter(o=>o!==e);if(c.length){s.set(t,c)}else{s.delete(t)}}};const i={get:function(t){return r.get(t)},set:function(t,e){r.set(t,e)}};exports.stateMap=i;exports.subscribers=a;
@@ -0,0 +1 @@
1
+ "use strict";var f=require("react");function u(e){return e&&e.__esModule?e:{default:e}}var t=u(f);const s=t.default.useInsertionEffect||t.default.useEffect;exports.useInsertionEffect=s;
@@ -0,0 +1 @@
1
+ "use strict";var e=require("./useSharedState.js");exports.useSharedState=e.useSharedState;
@@ -0,0 +1 @@
1
+ "use strict";var b=require("react");var u=require("../subscribers.js");var M=require("../useInsertionEffect.js");var i=require("../utils.js");function g(s){return s&&s.__esModule?s:{default:s}}var n=g(b);function v(s,a){const t=n.default.useRef(s);const[f,o]=n.default.useState(()=>{if(i.isSSR){return a?a?.():t.current}const e=u.stateMap.get(t.current);if(!e){const r=a?.()||t.current;u.stateMap.set(t.current,r);return r}else{return e}});const l=n.default.useCallback(e=>{const r=u.stateMap.get(t.current);const S=typeof e==="function";const c=S?e(r):{...r,...e};if(i.isEqual(r,c))return void 0;u.stateMap.set(t.current,c);u.subscribers.get(t.current).forEach(p=>p())},[]);M.useInsertionEffect(()=>{const e=()=>{o(u.stateMap.get(t.current)||t.current)};const r=u.subscribers.add(t.current,e);return()=>{r()}},[]);const d=n.default.useCallback(()=>u.stateMap.get(t.current)||t.current,[]);return{state:f,getState:d,setState:l}}exports.useSharedState=v;
@@ -0,0 +1 @@
1
+ "use strict";var e=require("./useUrlEncode.js");exports.useUrlEncode=e.useUrlEncode;
@@ -0,0 +1 @@
1
+ "use strict";var c=require("react");var r=require("../encodeState/encodeState.js");var l=require("../utils.js");function d(e){return e&&e.__esModule?e:{default:e}}var u=d(c);function i(e){const a=u.default.useCallback((t,s)=>{return l.typeOf(t)==="object"?r.encodeState(t,e,s):""},[e]);const n=u.default.useCallback(t=>r.decodeState(t,e),[e]);return{parse:n,stringify:a}}exports.useUrlEncode=i;
@@ -1 +1 @@
1
- import r from"react";import{encodeState as a,decodeState as s}from"../encodeState/encodeState.mjs";import{typeOf as f}from"../utils.mjs";function i(e){const o=r.useCallback((t,n)=>{return f(t)==="object"?a(t,e,n):""},[e]);const c=r.useCallback(t=>s(t,e),[e]);return{parse:c,stringify:o}}export{i as useUrlEncode};
1
+ import r from"react";import{encodeState as s,decodeState as a}from"../encodeState/encodeState.mjs";import{typeOf as f}from"../utils.mjs";function i(e){const o=r.useCallback((t,c)=>{return f(t)==="object"?s(t,e,c):""},[e]);const n=r.useCallback(t=>a(t,e),[e]);return{parse:n,stringify:o}}export{i as useUrlEncode};
@@ -0,0 +1 @@
1
+ "use strict";var e=require("./useUrlStateBase.js");exports.useUrlStateBase=e.useUrlStateBase;
@@ -0,0 +1 @@
1
+ "use strict";var y=require("react");var R=require("../constants/constants.js");var T=require("../useInsertionEffect.js");var _=require("../useSharedState/useSharedState.js");var b=require("../useUrlEncode/useUrlEncode.js");var B=require("../utils.js");function L(e){return e&&e.__esModule?e:{default:e}}var u=L(y);function P(e,t,i,l){const{parse:s,stringify:o}=b.useUrlEncode(e);const{state:f,getState:a,setState:r}=_.useSharedState(e,()=>i?.({parse:s})||e);const d=u.default.useRef(void 0);T.useInsertionEffect(()=>{const n=()=>{const p=s(B.filterUnknownParamsClient(e,I()));r(p)};window.addEventListener(q,n);return()=>{window.removeEventListener(q,n);clearTimeout(d.current)}},[r]);const w=u.default.useRef([]);const h=u.default.useCallback((n,p)=>{const S=typeof n==="function"?n(a()):{...a(),...n};const v=o(S,C(e));const U=M(window.location.pathname,l);const m=`${U}${v.length?"?":""}${v}${window.location.hash}`;const $=`${U}${window.location.search}${window.location.hash}`;if(m===$)return;r(S);const{replace:g,...k}=p||{};w.current.push([g?"replace":"push",m,k]);clearTimeout(d.current);const c=w.current.filter(Boolean).at(-1);if(c){queueMicrotask(()=>{d.current=setTimeout(()=>{w.current=[];t[c[0]](c[1],c[2])},R.TIMEOUT)})}},[t,o,a]);const E=u.default.useCallback(n=>{r(e);h(e,n)},[h,r]);return{updateState:r,updateUrl:h,state:f,reset:E,getState:a}}function C(e){const t=Object.keys(e);const i=window.location.search;const l=new URLSearchParams(i);const s=new URLSearchParams;for(const[o,f]of l){!t.includes(o)&&s.set(o,f)}return s}const q="popstate";function I(){return typeof window!=="undefined"&&window.location.search||""}function M(e,t){if(!t||t==="/"){return e}return e.slice(t.length)}exports.useUrlStateBase=P;
package/dist/utils.d.ts CHANGED
@@ -22,7 +22,7 @@ export type UnknownObj = object | {
22
22
  [key: string]: unknown;
23
23
  };
24
24
  export declare const isEqual: (val1: unknown, val2: unknown) => boolean;
25
- export declare function filterUnknownParamsClient<T extends object>(shape: T, params: string | URLSearchParams | Record<string, string> | object): string;
25
+ export declare function filterUnknownParamsClient<T extends object>(shape: T, params?: string | URLSearchParams | Record<string, string> | object): string;
26
26
  export declare function filterUnknownParams<T extends object>(shape: T, searchParams?: object): T;
27
27
  export declare function filterUnknown<T extends object>(shape: T, entries: [key: string, value: string][]): string[][];
28
28
  export declare function assignValue<T extends object>(shape: T, newVal: Partial<T>): T;
package/dist/utils.js ADDED
@@ -0,0 +1 @@
1
+ "use strict";const c=n=>{const t=typeof n;const r=n===null;const e=Array.isArray(n);const s=n instanceof Date;const o=!r&&!s&&!e&&t==="object";return r&&"null"||s&&"date"||e&&"array"||o&&"object"||t};const a=typeof window==="undefined";const f=n=>new URLSearchParams(typeof n==="string"?l(n):n?.toString?.()||"");const l=n=>n.split("?")?.[1]||n||"";const u=(n,t)=>JSON.stringify(n)===JSON.stringify(t);function w(n,t){const r=new URLSearchParams;const e=i(n,[...new URLSearchParams(t||"").entries()]);for(const[s,o]of e){r.set(s,o)}return r.toString()}function y(n,t){return Object.fromEntries(i(n,Object.entries(t||{})))}function i(n,t){const r=Object.keys(n);const e=[];for(let s=0;s<t.length;s++){if(r.includes(t[s]?.[0])){e.push([t[s][0].replace(/\+/g," "),t[s][1]])}}return e}function S(n,t){const r=Object.assign({},n);for(const[e]of Object.entries(n)){const s=e;const o=t[s]!==void 0;r[s]=o?t[s]:n[s]}return r}const g={push:n=>{window&&window.history.pushState(null,"",n)},replace:n=>{window&&window.history.replaceState(null,"",n)}};const p=/apple/i.test(!a&&navigator?.userAgent||"");exports.assignValue=S;exports.filterUnknown=i;exports.filterUnknownParams=y;exports.filterUnknownParamsClient=w;exports.getParams=f;exports.isEqual=u;exports.isSSR=a;exports.isSafari=p;exports.routerHistory=g;exports.typeOf=c;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "state-in-url",
3
- "version": "4.3.3",
4
- "description": "Store state in URL as in object, types and structure are preserved, with TS validation. Same API as React.useState, wthout any hasssle or boilerplate. Next.js@14-15 and react-router@6-7.",
3
+ "version": "5.1.0",
4
+ "description": "Store state in URL as in object, types and structure are preserved, with TS validation. Same API as React.useState, wthout any hasssle or boilerplate. Next.js@14-15, react-router@6-7, and remix@2.",
5
5
  "homepage": "https://state-in-url.dev",
6
6
  "repository": {
7
7
  "type": "git",
@@ -30,48 +30,64 @@
30
30
  "packages/example-nextjs14",
31
31
  "packages/example-nextjs15",
32
32
  "packages/example-react",
33
- "packages/example-react-router6"
33
+ "packages/example-react-router6",
34
+ "packages/example-remix2"
34
35
  ],
35
36
  "files": [
36
37
  "dist"
37
38
  ],
38
- "main": "dist/index.mjs",
39
+ "main": "./dist/index.js",
40
+ "module": "dist/index.mjs",
39
41
  "exports": {
40
42
  ".": {
41
43
  "types": "./dist/index.d.ts",
42
- "default": "./dist/index.mjs"
44
+ "default": "./dist/index.mjs",
45
+ "require": "./dist/index.js"
43
46
  },
44
47
  "./next": {
45
48
  "types": "./dist/next/index.d.ts",
46
- "default": "./dist/next/index.mjs"
49
+ "default": "./dist/next/index.mjs",
50
+ "require": "./dist/next/index.js"
47
51
  },
48
52
  "./react-router": {
49
53
  "types": "./dist/react-router/index.d.ts",
50
- "default": "./dist/react-router/index.mjs"
54
+ "default": "./dist/react-router/index.mjs",
55
+ "require": "./dist/react-router/index.js"
56
+ },
57
+ "./remix": {
58
+ "types": "./dist/remix/index.d.ts",
59
+ "default": "./dist/remix/index.mjs",
60
+ "require": "./dist/remix/index.js"
51
61
  },
52
62
  "./encodeState": {
53
63
  "types": "./dist/encodeState/index.d.ts",
54
- "default": "./dist/encodeState/index.mjs"
64
+ "default": "./dist/encodeState/index.mjs",
65
+ "require": "./dist/encodeState/index.js"
55
66
  },
56
67
  "./encoder": {
57
68
  "types": "./dist/encoder/index.d.ts",
58
- "default": "./dist/encoder/index.mjs"
69
+ "default": "./dist/encoder/index.mjs",
70
+ "require": "./dist/encoder/index.js"
59
71
  },
60
72
  "./useSharedState": {
61
73
  "types": "./dist/useSharedState/index.d.ts",
62
- "default": "./dist/useSharedState/index.mjs"
74
+ "default": "./dist/useSharedState/index.mjs",
75
+ "require": "./dist/useSharedState/index.js"
63
76
  },
64
77
  "./useUrlEncode": {
65
78
  "types": "./dist/useUrlEncode/index.d.ts",
66
- "default": "./dist/useUrlEncode/index.mjs"
79
+ "default": "./dist/useUrlEncode/index.mjs",
80
+ "require": "./dist/useUrlEncode/index.js"
67
81
  },
68
82
  "./useUrlStateBase": {
69
83
  "types": "./dist/useUrlStateBase/index.d.ts",
70
- "default": "./dist/useUrlStateBase/index.mjs"
84
+ "default": "./dist/useUrlStateBase/index.mjs",
85
+ "require": "./dist/useUrlStateBase/index.js"
71
86
  },
72
87
  "./utils": {
73
88
  "types": "./dist/utils.d.ts",
74
- "default": "./dist/utils.mjs"
89
+ "default": "./dist/utils.mjs",
90
+ "require": "./dist/utils.js"
75
91
  }
76
92
  },
77
93
  "types": "./dist/index.d.ts",
@@ -80,11 +96,14 @@
80
96
  "test:int": "wireit",
81
97
  "test:int:ci": "wireit",
82
98
  "test:exports": "npm run build && npx babel-node --extensions .js,.mjs,.ts,.mts --config-file ./babel.config.json -- ./packages/urlstate/exportsTest.ts",
99
+ "install:all": "npm install",
100
+ "install:ci": "npm ci",
83
101
  "test": "wireit",
84
102
  "tsc": "wireit",
85
103
  "dev": "wireit",
86
104
  "dev:lib": "wireit",
87
105
  "dev:nextjs14": "wireit",
106
+ "dev:remix2": "wireit",
88
107
  "bump": "npx standard-version --no-verify",
89
108
  "build": "wireit",
90
109
  "build:demo": "wireit",
@@ -99,7 +118,7 @@
99
118
  "kill-vite": "wireit",
100
119
  "kill": "wireit",
101
120
  "rmdist": "rm -rf dist",
102
- "cleanup": "rm -rf dist .wireit node_modules test-results playwright-report coverage-reports nyc_coverage packages/example-nextjs14/node_modules packages/example-nextjs14/.next packages/example-nextjs15/node_modules packages/example-nextjs15/.next packages/example-react/node_modules packages/example-react-router6/node_modules",
121
+ "cleanup": "rm -rf dist .wireit node_modules test-results playwright-report coverage-reports nyc_coverage packages/example-nextjs14/node_modules packages/example-nextjs14/.next packages/example-nextjs15/node_modules packages/example-nextjs15/.next packages/example-react/node_modules packages/example-react-router6/node_modules packages/example-remix2/node_modules",
103
122
  "reinstall": "npm run cleanup && npm install",
104
123
  "setup": "playwright install --with-deps",
105
124
  "prepack": "npm run build && cp package.json dist/package.json && npm run build:demo",
@@ -151,6 +170,7 @@
151
170
  "dependencies": [
152
171
  "dev:lib",
153
172
  "dev:react",
173
+ "dev:remix2",
154
174
  "dev:router6",
155
175
  "dev:nextjs15"
156
176
  ],
@@ -176,6 +196,10 @@
176
196
  "command": "cd packages/example-react-router6 && npm run dev",
177
197
  "service": true
178
198
  },
199
+ "dev:remix2": {
200
+ "command": "cd packages/example-remix2 && PORT=3003 npm run dev",
201
+ "service": true
202
+ },
179
203
  "dev:nextjs14": {
180
204
  "command": "cd packages/example-nextjs14 && npm run dev --turbo",
181
205
  "service": true
@@ -196,6 +220,12 @@
196
220
  "build"
197
221
  ]
198
222
  },
223
+ "build:remix2": {
224
+ "command": "cd packages/example-remix2 && npm run build",
225
+ "dependencies": [
226
+ "build"
227
+ ]
228
+ },
199
229
  "build:demo": {
200
230
  "dependencies": [
201
231
  "build"
@@ -241,6 +271,17 @@
241
271
  "command": "npx vite preview --port 5181 packages/example-react-router6",
242
272
  "service": true
243
273
  },
274
+ "start:remix2": {
275
+ "command": "cd packages/example-remix2 && npm run start",
276
+ "dependencies": [
277
+ "build:remix2"
278
+ ],
279
+ "service": true
280
+ },
281
+ "start:remix2:ci": {
282
+ "command": "cd packages/example-remix2 && npm run start",
283
+ "service": true
284
+ },
244
285
  "start:demo15": {
245
286
  "dependencies": [
246
287
  "build:next15"
@@ -257,7 +298,8 @@
257
298
  "dependencies": [
258
299
  "start:demo",
259
300
  "start:react",
260
- "start:router6"
301
+ "start:router6",
302
+ "start:remix2"
261
303
  ],
262
304
  "service": true
263
305
  },
@@ -266,7 +308,8 @@
266
308
  "dependencies": [
267
309
  "start:demo:ci",
268
310
  "start:react:ci",
269
- "start:router6:ci"
311
+ "start:router6:ci",
312
+ "start:remix2:ci"
270
313
  ],
271
314
  "service": true
272
315
  },
@@ -280,7 +323,8 @@
280
323
  "build:react",
281
324
  "build:demo:tests",
282
325
  "build:next15",
283
- "build:router6"
326
+ "build:router6",
327
+ "build:remix2"
284
328
  ]
285
329
  },
286
330
  "build:react": {
@@ -404,6 +448,8 @@
404
448
  "decode",
405
449
  "Next.js",
406
450
  "react-router",
451
+ "remix",
452
+ "remixjs",
407
453
  "url",
408
454
  "url synchronization",
409
455
  "url sync",