state-in-url 3.0.7 → 4.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
@@ -144,31 +144,30 @@ import { useUrlState } from 'state-in-url/next';
144
144
  import { userState } from './userState';
145
145
 
146
146
  function MyComponent() {
147
- // can pass `replace` arg, it's control will `updateUrl` will use `rounter.push` or `router.replace`, default replace=true
148
- // const { state, updateState, updateUrl } = useUrlState({ defaultState: userState, searchParams, replace: false });
149
- const { state, updateState, updateUrl } = useUrlState({ defaultState: userState });
147
+ // can pass `replace` arg, it's control will `setUrl` will use `rounter.push` or `router.replace`, default replace=true
148
+ // can pass `searchParams` from server components
149
+ const { urlState, setUrl, setUrlState } = useUrlState({ defaultState: userState });
150
150
 
151
151
  // won't let you to accidently mutate state directly, requires TS
152
- // state.name = 'John' // <- error
152
+ // urlState.name = 'John' // <- error
153
153
 
154
154
  return (
155
155
  <div>
156
- <input value={state.name}
157
- onChange={(ev) => { updateState({ name: ev.target.value }) }}
158
- onBlur={() => updateUrl()}
156
+ <input value={urlState.name}
157
+ onChange={(ev) => { setUrl({ name: ev.target.value }) }}
159
158
  />
160
- <input value={state.age}
161
- onChange={(ev) => { updateState({ age: +ev.target.value }) }}
162
- onBlur={() => updateUrl()}
159
+ <input value={urlState.age}
160
+ onChange={(ev) => { setUrl({ age: +ev.target.value }) }}
163
161
  />
164
162
 
165
163
  // same api as React.useState
166
- <input value={state.name}
167
- onChange={(ev) => { updateState(curr => ({ ...curr, name: ev.target.value })) }}
168
- onBlur={() => updateUrl()}
164
+ <input value={urlState.name}
165
+ onChange={(ev) => { setUrlState(curr => ({ ...curr, name: ev.target.value })) }}
166
+ // Can update state immediately but sync change to url as needed
167
+ onBlur={() => setUrl()}
169
168
  />
170
169
 
171
- <button onClick={() => updateUrl(userState)}>
170
+ <button onClick={() => setUrl(userState)}>
172
171
  Reset
173
172
  </button>
174
173
 
@@ -181,7 +180,7 @@ function MyComponent() {
181
180
 
182
181
  <details>
183
182
  <Summary>Example</Summary>
184
-
183
+
185
184
  ```typescript
186
185
  export const form: Form = {
187
186
  name: '',
@@ -205,19 +204,19 @@ import { useUrlState } from 'state-in-url/next';
205
204
  import { form } from './form';
206
205
 
207
206
  function TagsComponent() {
208
- // `state` will infer from Form type!
209
- const { state, updateUrl } = useUrlState({ defaultState: form });
207
+ // `urlState` will infer from Form type!
208
+ const { urlState, setUrl } = useUrlState({ defaultState: form });
210
209
 
211
210
  const onChangeTags = React.useCallback(
212
211
  (tag: (typeof tags)[number]) => {
213
- updateUrl((curr) => ({
212
+ setUrl((curr) => ({
214
213
  ...curr,
215
214
  tags: curr.tags.find((t) => t.id === tag.id)
216
215
  ? curr.tags.filter((t) => t.id !== tag.id)
217
216
  : curr.tags.concat(tag),
218
217
  }));
219
218
  },
220
- [updateUrl],
219
+ [setUrl],
221
220
  );
222
221
 
223
222
  return (
@@ -226,7 +225,7 @@ function TagsComponent() {
226
225
  <div className="flex flex-wrap gap-2">
227
226
  {tags.map((tag) => (
228
227
  <Tag
229
- active={!!state.tags.find((t) => t.id === tag.id)}
228
+ active={!!urlState.tags.find((t) => t.id === tag.id)}
230
229
  text={tag.value.text}
231
230
  onClick={() => onChangeTags(tag)}
232
231
  key={tag.id}
@@ -257,26 +256,25 @@ const tags = [
257
256
  [Demo page example code](https://github.com/asmyshlyaev177/state-in-url/blob/master/packages/example-nextjs14/src/app/Form.tsx)
258
257
  </details>
259
258
 
260
-
261
- #### Auto sync state
259
+ #### Update state only and sync to URL manually
262
260
 
263
261
  <details>
264
262
  <Summary>Example</Summary>
265
263
 
266
264
  ```typescript
267
-
265
+
268
266
  const timer = React.useRef(0 as unknown as NodeJS.Timeout);
269
267
  React.useEffect(() => {
270
268
  clearTimeout(timer.current);
271
269
  timer.current = setTimeout(() => {
272
270
  // will compare state by content not by reference and fire update only for new values
273
- updateUrl(state);
271
+ setUrl(urlState);
274
272
  }, 500);
275
273
 
276
274
  return () => {
277
275
  clearTimeout(timer.current);
278
276
  };
279
- }, [state, updateUrl]);
277
+ }, [urlState, setUrl]);
280
278
  ```
281
279
 
282
280
  Syncing state `onBlur` will be more aligned with real world usage.
@@ -287,13 +285,11 @@ Syncing state `onBlur` will be more aligned with real world usage.
287
285
 
288
286
  </details>
289
287
 
290
-
291
-
292
288
  #### With server side rendering
293
289
 
294
290
  <details>
295
291
  <Summary>Example</Summary>
296
-
292
+
297
293
  ```typescript
298
294
  export default async function Home({ searchParams }: { searchParams: object }) {
299
295
  return (
@@ -308,7 +304,7 @@ import { useUrlState } from 'state-in-url/next';
308
304
  import { form } from './form';
309
305
 
310
306
  const Form = ({ searchParams }: { searchParams: object }) => {
311
- const { state, updateState, updateUrl } = useUrlState({ defaultState: form, searchParams });
307
+ const { urlState, setState, setUrl } = useUrlState({ defaultState: form, searchParams });
312
308
  }
313
309
  ```
314
310
 
@@ -362,6 +358,7 @@ export default async function Layout({
362
358
 
363
359
 
364
360
  ```
361
+
365
362
  </details>
366
363
 
367
364
  #### With arbitrary state shape (not recommended)
@@ -376,12 +373,11 @@ import { useUrlState } from 'state-in-url/next';
376
373
  const someObj = {};
377
374
 
378
375
  function SettingsComponent() {
379
- const { state, updateUrl, updateState } = useUrlState<object>(someObj);
376
+ const { urlState, setUrl, setState } = useUrlState<object>(someObj);
380
377
  }
381
378
  ```
382
- </details>
383
-
384
379
 
380
+ </details>
385
381
 
386
382
  ## useUrlState hook for React-Router
387
383
 
@@ -414,18 +410,18 @@ import { useUrlState } from 'state-in-url/react-router';
414
410
  import { form } from './form';
415
411
 
416
412
  function TagsComponent() {
417
- const { state, updateUrl } = useUrlState({ defaultState: form });
413
+ const { urlState, setUrl, setUrlState } = useUrlState({ defaultState: form });
418
414
 
419
415
  const onChangeTags = React.useCallback(
420
416
  (tag: (typeof tags)[number]) => {
421
- updateUrl((curr) => ({
417
+ setUrl((curr) => ({
422
418
  ...curr,
423
419
  tags: curr.tags.find((t) => t.id === tag.id)
424
420
  ? curr.tags.filter((t) => t.id !== tag.id)
425
421
  : curr.tags.concat(tag),
426
422
  }));
427
423
  },
428
- [updateUrl],
424
+ [setUrl],
429
425
  );
430
426
 
431
427
  return (
@@ -434,7 +430,7 @@ function TagsComponent() {
434
430
  <div className="flex flex-wrap gap-2">
435
431
  {tags.map((tag) => (
436
432
  <Tag
437
- active={!!state.tags.find((t) => t.id === tag.id)}
433
+ active={!!urlState.tags.find((t) => t.id === tag.id)}
438
434
  text={tag.value.text}
439
435
  onClick={() => onChangeTags(tag)}
440
436
  key={tag.id}
@@ -442,6 +438,12 @@ function TagsComponent() {
442
438
  ))}
443
439
  </div>
444
440
  </Field>
441
+
442
+ <input value={urlState.name}
443
+ onChange={(ev) => { setUrlState(curr => ({ ...curr, name: ev.target.value })) }}
444
+ // Can update state immediately but sync change to url as needed
445
+ onBlur={() => setUrl()}
446
+ />
445
447
  </div>
446
448
  );
447
449
  }
@@ -506,8 +508,6 @@ function SettingsComponent() {
506
508
  - Define your state shape as a constant
507
509
  - Use TypeScript for enhanced type safety and autocomplete
508
510
  - Avoid storing sensitive information in URL parameters (SSN, API keys etc)
509
- - Use `updateState` for frequent updates and `updateUrl` to sync changes to url
510
- - Use `Suspence` to wrap client components in Next.js
511
511
  - Use this [extension](https://marketplace.visualstudio.com/items?itemName=yoavbls.pretty-ts-errors) for readable TS errors
512
512
 
513
513
  ## Gothas
@@ -1,55 +1,69 @@
1
1
  import { useRouter } from "next/navigation";
2
2
  import { type DeepReadonly, type JSONCompatible } from "../../utils";
3
3
  /**
4
- * NextJS hook. Returns `state`, `updateState`, and `updateUrl` functions
4
+ * NextJS hook. Returns `urlState`, `setState`, and `setUrl` functions
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
11
12
  * export const form = { name: '', age: 0 };
12
- * const { state, updateState, updateUrl } = useUrlState({ defaultState: form });
13
+ * const { urlState, setState, setUrl } = useUrlState({ defaultState: form });
13
14
  * // for nextjs seerver components
14
- * // const { state, updateState, updateUrl } = useUrlState({ defaultState: form, searchParams });
15
+ * // const { urlState, setState, setUrl } = useUrlState({ defaultState: form, searchParams });
15
16
  *
16
- * updateState({ name: 'test' });
17
+ * setState({ name: 'test' });
17
18
  * // by default it's uses router.push with scroll: false
18
- * updateUrl({ name: 'test' }, { replace: true, scroll: true });
19
+ * setUrl({ name: 'test' }, { replace: true, scroll: true });
19
20
  * // similar to React.useState
20
- * updateUrl(curr => ({ ...curr, name: 'test' }), { replace: true, scroll: true });
21
+ * setUrl(curr => ({ ...curr, name: 'test' }), { replace: true, scroll: true });
21
22
  * ```
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:
33
35
  * ```ts
34
- * updateState({ name: 'test' });
36
+ * setState({ name: 'test' });
35
37
  * // or
36
- * updateState(curr => ({ ...curr, name: 'test' }) );
38
+ * setState(curr => ({ ...curr, name: 'test' }) );
37
39
  * ```
38
40
  *
39
41
  * * Docs {@link https://github.com/asmyshlyaev177/state-in-url/tree/master/packages/urlstate/next/useUrlState#updatestate}
40
42
  */
43
+ setState: (value: Partial<T> | Partial<DeepReadonly<T>> | ((currState: T) => T)) => void;
44
+ /**
45
+ * @deprecated use `setState`
46
+ */
41
47
  updateState: (value: Partial<T> | Partial<DeepReadonly<T>> | ((currState: T) => T)) => void;
42
48
  /**
43
49
  * * Example:
44
50
  * ```ts
45
- * updateUrl({ name: 'test' });
51
+ * setUrl({ name: 'test' });
46
52
  * // or
47
- * updateUrl(curr => ({ ...curr, name: 'test' }), { replace: true, scroll: false } );
53
+ * setUrl(curr => ({ ...curr, name: 'test' }), { replace: true, scroll: false } );
48
54
  * ```
49
55
  *
50
56
  * * Docs {@link https://github.com/asmyshlyaev177/state-in-url/tree/master/packages/urlstate/next/useUrlState#updateurl}
51
57
  */
58
+ setUrl: (value?: Parameters<(value?: Partial<T> | Partial<DeepReadonly<T>> | ((currState: T) => T) | undefined, options?: import("../../useUrlStateBase/useUrlStateBase").Options) => void>[0], options?: Options) => void;
59
+ /**
60
+ * @deprecated use `setUrl`
61
+ */
52
62
  updateUrl: (value?: Parameters<(value?: Partial<T> | Partial<DeepReadonly<T>> | ((currState: T) => T) | undefined, options?: import("../../useUrlStateBase/useUrlStateBase").Options) => void>[0], options?: Options) => void;
63
+ urlState: DeepReadonly<DeepReadonly<T>>;
64
+ /**
65
+ * @deprecated use `urlState`
66
+ */
53
67
  state: DeepReadonly<DeepReadonly<T>>;
54
68
  getState: () => T;
55
69
  };
@@ -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 p}from"../../utils.mjs";function u({defaultState:u,searchParams:i,...l}){const n=t(),{state:f,updateState:S,updateUrl:d,getState:j}=s(u,n,(({parse:t})=>o()?r(m(u,i),u):t(p(u)))),U=a.useCallback(((t,e)=>{const a={...c,...l,...e};d(t,a)}),[d,l]),b=e();return a.useEffect((()=>{S(m(u,r(Object.fromEntries([...b.entries()]),u)))}),[b]),{updateState:S,updateUrl:U,state:f,getState:j}}const c={replace:!0,scroll:!1};export{u 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};
@@ -1,52 +1,66 @@
1
1
  import { type NavigateOptions } from "react-router-dom";
2
2
  import { type DeepReadonly, type JSONCompatible } from "../../utils";
3
3
  /**
4
- * React-router hook. Returns `state`, `updateState`, and `updateUrl` functions
4
+ * React-router hook. Returns `urlState`, `setState`, and `setUrl` functions
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 };
11
- * const { state, updateState, updateUrl } = useUrlState({ defaultState: form, replace: false, preventScrollReset: false });
12
+ * const { urlState, setState, setUrl } = useUrlState({ defaultState: form, replace: false, preventScrollReset: false });
12
13
  *
13
- * updateState({ name: 'test' });
14
- * updateUrl({ name: 'test' }, { replace: true });
14
+ * setState({ name: 'test' });
15
+ * setUrl({ name: 'test' }, { replace: true });
15
16
  * // similar to React.useState
16
- * updateUrl(curr => ({ ...curr, name: 'test' }), { replace: true });
17
+ * setUrl(curr => ({ ...curr, name: 'test' }), { replace: true });
17
18
  * ```
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
- * updateState({ name: 'test' });
29
+ * setUrlState({ name: 'test' });
28
30
  * // or
29
- * updateState(curr => ({ ...curr, name: 'test' }) );
31
+ * setUrlState(curr => ({ ...curr, name: 'test' }) );
30
32
  * // can pass optional React-Router `NavigateOptions`
31
- * updateState(curr => ({ ...curr, name: 'test', preventScrollReset: false }) );
33
+ * setUrlState(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}
35
37
  */
38
+ setState: (value: Partial<T> | Partial<DeepReadonly<T>> | ((currState: T) => T)) => void;
39
+ /**
40
+ * @deprecated use `setState`
41
+ */
36
42
  updateState: (value: Partial<T> | Partial<DeepReadonly<T>> | ((currState: T) => T)) => void;
37
43
  /**
38
44
  * * Example:
39
45
  * ```ts
40
- * updateUrl({ name: 'test' });
46
+ * setUrl({ name: 'test' });
41
47
  * // or
42
- * updateUrl(curr => ({ ...curr, name: 'test' }), { replace: true } );
48
+ * setUrl(curr => ({ ...curr, name: 'test' }), { replace: true } );
43
49
  * // can pass optional React-Router `NavigateOptions`
44
- * updateState(curr => ({ ...curr, name: 'test', preventScrollReset: false }) );
50
+ * setState(curr => ({ ...curr, name: 'test', preventScrollReset: false }) );
45
51
  * ```
46
52
  *
47
53
  * * Docs {@link https://github.com/asmyshlyaev177/state-in-url/tree/master/packages/urlstate/react-router/useUrlState#updateurl}
48
54
  */
55
+ setUrl: (value?: Parameters<(value?: Partial<T> | Partial<DeepReadonly<T>> | ((currState: T) => T) | undefined, options?: import("../../useUrlStateBase/useUrlStateBase").Options) => void>[0], options?: NavigateOptions) => void;
56
+ /**
57
+ * @deprecated use `setUrl`
58
+ */
49
59
  updateUrl: (value?: Parameters<(value?: Partial<T> | Partial<DeepReadonly<T>> | ((currState: T) => T) | undefined, options?: import("../../useUrlStateBase/useUrlStateBase").Options) => void>[0], options?: NavigateOptions) => void;
60
+ urlState: DeepReadonly<DeepReadonly<T>>;
61
+ /**
62
+ * @deprecated use `urlState`
63
+ */
50
64
  state: DeepReadonly<DeepReadonly<T>>;
51
65
  getState: () => T;
52
66
  };
@@ -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,...l}){const f=e(),i=t.useMemo((()=>({replace:(t,e)=>f(t,{...c,...l,...e}),push:(t,e)=>f(t,{...c,...l,...e})})),[f,l]),{state:n,updateState:S,updateUrl:d,getState:j}=s(u,i,(({parse:t})=>t(o(u)))),U=t.useCallback(((t,e)=>{const r={...c,...l,...e};d(t,r)}),[l]),[b]=r();return t.useEffect((()=>{S(m(u,n,p(u,a(Object.fromEntries([...b.entries()]),u))))}),[b]),{updateState:S,updateUrl:U,state:n,getState:j}}const c={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 o}from"../useSharedState/useSharedState.mjs";import{useUrlEncode as n}from"../useUrlEncode/useUrlEncode.mjs";import{filterUnknownParamsClient as a}from"../utils.mjs";function s(s,c,i){const{parse:m,stringify:p}=n(s),{state:w,getState:d,setState:u}=o(s,(()=>i?.({parse:m})||s));e((()=>{const t=()=>{const t=m(a(s));u(t)};return window.addEventListener(r,t),()=>{window.removeEventListener(r,t)}}),[u]);const l=t.useCallback(((t,e)=>{const o=`${window.location.pathname}${window.location.search}${window.location.hash}`,n="function"==typeof t,a=function(t){const e=Object.keys(t),o=window.location.search,n=new URLSearchParams(o),a=new URLSearchParams;return n.forEach(((t,o)=>!e.includes(o)&&a.set(o,t))),a}(s),r=n?t(d()):t?{...d(),...t}:d(),i=p(r,a);u(r);const m=`${window.location.pathname}${i.length?"?":""}${i}${window.location.hash}`;if(o!==m){const{replace:t,...o}=e||{};c[t?"replace":"push"](m,{...o})}}),[c,p,d]);return{updateState:u,updateUrl:l,state:w,getState:d}}const r="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.0.7",
3
+ "version": "4.0.0",
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": [
@@ -350,7 +350,7 @@
350
350
  "@babel/preset-typescript": "^7.24.7",
351
351
  "@commitlint/cli": "^19.0.3",
352
352
  "@commitlint/config-conventional": "^19.0.3",
353
- "@playwright/test": "^1.45.3",
353
+ "@playwright/test": "^1.48.2",
354
354
  "@rollup/plugin-node-resolve": "^15.3.0",
355
355
  "@rollup/plugin-terser": "^0.4.4",
356
356
  "@rollup/plugin-typescript": "^12.1.0",
@@ -386,7 +386,7 @@
386
386
  "jest-environment-jsdom": "^29.7.0",
387
387
  "lint-staged": "^15.2.9",
388
388
  "only-allow": "^1.2.1",
389
- "playwright": "^1.45.3",
389
+ "playwright": "^1.48.2",
390
390
  "prettier": "^3.2.5",
391
391
  "prettier-plugin-tailwindcss": "^0.6.8",
392
392
  "release-it": "^17.6.0",