state-in-url 5.1.0 → 5.2.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
@@ -99,26 +99,46 @@ This library is a good alternative for NUQS.
99
99
 
100
100
  ## Table of content
101
101
 
102
- - [Installation](#installation)
103
- - [useUrlState hook](#useurlstate)
104
- - - [Next.js](#useurlstate-hook-for-nextjs)
105
- - - [Remix](#useurlstate-hook-for-remixjs)
106
- - - [React-Router](#useurlstate-hook-for-react-router)
107
- - [Other helpers](#other-hooks-and-helpers)
108
- - - [`useUrlStateBase` for other routers](#useurlstatebase-hook-for-others-routers)
109
- - - [`useSharedState` hook for React.js/Next.js](#usesharedstate-hook-for-reactjs)
110
- - - [`useUrlEncode` for React.js](#useurlencode-hook-for-reactjs)
111
- - - [`encodeState` and `decodeState` for pure JS usage](#encodestate-and-decodestate-helpers)
112
- - - [Low-level `encode` and `decode` functions](#encode-and-decode-helpers)
113
- - [Best practices](#best-practices)
114
- - - [Gotchas](#gotchas)
115
- - [Other](#other)
116
- - - [Roadmap](#roadmap)
117
- - - [Contributing](#contribute-andor-run-locally)
118
- - - [Contact & Support](#contact--support)
119
- - - [Changelog](#changelog)
120
- - - [License](#license)
121
- - - [Inspiration](#inspiration)
102
+ - [State in url](#state-in-url)
103
+ - [Demo](#demo)
104
+ - [Why use `state-in-url`?](#why-use-state-in-url)
105
+ - [Use cases](#use-cases)
106
+ - [Features](#features)
107
+ - [Table of content](#table-of-content)
108
+ - [installation](#installation)
109
+ - [1. Install package](#1-install-package)
110
+ - [2. Edit tsconfig.json](#2-edit-tsconfigjson)
111
+ - [useUrlState](#useurlstate)
112
+ - [useUrlState hook for Next.js](#useurlstate-hook-for-nextjs)
113
+ - [Usage examples](#usage-examples)
114
+ - [Basic](#basic)
115
+ - [With server side rendering](#with-server-side-rendering)
116
+ - [Using hook in `layout` component](#using-hook-in-layout-component)
117
+ - [With arbitrary state shape (not recommended)](#with-arbitrary-state-shape-not-recommended)
118
+ - [useUrlState hook for Remix.js](#useurlstate-hook-for-remixjs)
119
+ - [Example](#example)
120
+ - [useUrlState hook for React-Router](#useurlstate-hook-for-react-router)
121
+ - [Example](#example-1)
122
+ - [Recipes](#recipes)
123
+ - [Custom hook to work with slice of state conveniently](#custom-hook-to-work-with-slice-of-state-conveniently)
124
+ - [With complex state shape](#with-complex-state-shape)
125
+ - [Update state only and sync to URL manually](#update-state-only-and-sync-to-url-manually)
126
+ - [Other hooks and helpers](#other-hooks-and-helpers)
127
+ - [`useUrlStateBase` hook for others routers](#useurlstatebase-hook-for-others-routers)
128
+ - [`useSharedState` hook for React.js](#usesharedstate-hook-for-reactjs)
129
+ - [`useUrlEncode` hook for React.js](#useurlencode-hook-for-reactjs)
130
+ - [`encodeState` and `decodeState` helpers](#encodestate-and-decodestate-helpers)
131
+ - [`encode` and `decode` helpers](#encode-and-decode-helpers)
132
+ - [Best Practices](#best-practices)
133
+ - [Gotchas](#gotchas)
134
+ - [Other](#other)
135
+ - [Contribute and/or run locally](#contribute-andor-run-locally)
136
+ - [Roadmap](#roadmap)
137
+ - [Contact \& Support](#contact--support)
138
+ - [Changelog](#changelog)
139
+ - [Mentions](#mentions)
140
+ - [License](#license)
141
+ - [Inspiration](#inspiration)
122
142
 
123
143
  ## installation
124
144
 
@@ -175,7 +195,7 @@ import { userState } from './userState';
175
195
  function MyComponent() {
176
196
  // can pass `replace` arg, it's control will `setUrl` will use `rounter.push` or `router.replace`, default replace=true
177
197
  // can pass `searchParams` from server components, pass `useHistory: false` if you need to fetch smt in the server component
178
- const { urlState, setUrl, setState, reset } = useUrlState(userState);
198
+ const { urlState, setUrl, setState } = useUrlState(userState);
179
199
 
180
200
  return (
181
201
  <div>
@@ -194,7 +214,7 @@ function MyComponent() {
194
214
  onBlur={() => setUrl()}
195
215
  />
196
216
 
197
- <button onClick={reset}>
217
+ <button onClick={() => setUrl((_, initial) => initial)}>
198
218
  Reset
199
219
  </button>
200
220
 
@@ -203,164 +223,6 @@ function MyComponent() {
203
223
  }
204
224
  ```
205
225
 
206
- ##### Custom hook to work with slice of state conveniently
207
- <details>
208
- <Summary>Example</Summary>
209
-
210
- ```typescript
211
- 'use client';
212
-
213
- import React from 'react';
214
- import { useUrlState } from 'state-in-url/next';
215
-
216
- const form: Form = {
217
- name: '',
218
- age: undefined,
219
- agree_to_terms: false,
220
- tags: [],
221
- };
222
-
223
- type Form = {
224
- name: string;
225
- age?: number;
226
- agree_to_terms: boolean;
227
- tags: {id: string; value: {text: string; time: Date } }[];
228
- };
229
-
230
- export const useFormState = ({ searchParams }: { searchParams?: object }) => {
231
- const { urlState, setUrl: setUrlBase, reset } = useUrlState(form, {
232
- searchParams,
233
- });
234
-
235
- // first navigation will push new history entry
236
- // all following will just replace that entry
237
- // this way will have history with only 2 entries - ['/url', '/url?key=param']
238
-
239
- const replace = React.useRef(false);
240
- const setUrl = React.useCallback((
241
- state: Parameters<typeof setUrlBase>[0],
242
- opts?: Parameters<typeof setUrlBase>[1]
243
- ) => {
244
- setUrlBase(state, { replace: replace.current, ...opts });
245
- replace.current = true;
246
- }, [setUrlBase]);
247
-
248
- return { urlState, setUrl, resetUrl: reset };
249
- };
250
- ```
251
- </details>
252
-
253
- <hr />
254
-
255
- ##### With complex state shape
256
-
257
- <details>
258
- <Summary>Example</Summary>
259
-
260
- ```typescript
261
- export const form: Form = {
262
- name: '',
263
- age: undefined,
264
- agree_to_terms: false,
265
- tags: [],
266
- };
267
-
268
- type Form = {
269
- name: string;
270
- age?: number;
271
- agree_to_terms: boolean;
272
- tags: { id: string; value: { text: string; time: Date } }[];
273
- };
274
- ```
275
-
276
- ```typescript
277
- 'use client'
278
- import { useUrlState } from 'state-in-url/next';
279
-
280
- import { form } from './form';
281
-
282
- function TagsComponent() {
283
- // `urlState` will infer from Form type!
284
- const { urlState, setUrl } = useUrlState(form);
285
-
286
- const onChangeTags = React.useCallback(
287
- (tag: (typeof tags)[number]) => {
288
- setUrl((curr) => ({
289
- ...curr,
290
- tags: curr.tags.find((t) => t.id === tag.id)
291
- ? curr.tags.filter((t) => t.id !== tag.id)
292
- : curr.tags.concat(tag),
293
- }));
294
- },
295
- [setUrl],
296
- );
297
-
298
- return (
299
- <div>
300
- <Field text="Tags">
301
- <div className="flex flex-wrap gap-2">
302
- {tags.map((tag) => (
303
- <Tag
304
- active={!!urlState.tags.find((t) => t.id === tag.id)}
305
- text={tag.value.text}
306
- onClick={() => onChangeTags(tag)}
307
- key={tag.id}
308
- />
309
- ))}
310
- </div>
311
- </Field>
312
- </div>
313
- );
314
- }
315
-
316
- const tags = [
317
- {
318
- id: '1',
319
- value: { text: 'React.js', time: new Date('2024-07-17T04:53:17.000Z') },
320
- },
321
- {
322
- id: '2',
323
- value: { text: 'Next.js', time: new Date('2024-07-18T04:53:17.000Z') },
324
- },
325
- {
326
- id: '3',
327
- value: { text: 'TailwindCSS', time: new Date('2024-07-19T04:53:17.000Z') },
328
- },
329
- ];
330
- ```
331
-
332
- [Demo page example code](https://github.com/asmyshlyaev177/state-in-url/blob/master/packages/example-nextjs14/src/app/Form.tsx)
333
- </details>
334
-
335
- ##### Update state only and sync to URL manually
336
-
337
- <details>
338
- <Summary>Example</Summary>
339
-
340
- ```typescript
341
-
342
- const timer = React.useRef(0 as unknown as NodeJS.Timeout);
343
- React.useEffect(() => {
344
- clearTimeout(timer.current);
345
- timer.current = setTimeout(() => {
346
- // will compare state by content not by reference and fire update only for new values
347
- setUrl(urlState);
348
- }, 500);
349
-
350
- return () => {
351
- clearTimeout(timer.current);
352
- };
353
- }, [urlState, setUrl]);
354
- ```
355
-
356
- Syncing state `onBlur` will be more aligned with real world usage.
357
-
358
- ```typescript
359
- <input onBlur={() => updateUrl()} .../>
360
- ```
361
-
362
- </details>
363
-
364
226
  ##### With server side rendering
365
227
 
366
228
  <details>
@@ -629,6 +491,165 @@ const tags = [
629
491
 
630
492
  [Example code](packages/example-react-router6/src/Form-for-test.tsx)
631
493
 
494
+ ## Recipes
495
+ ##### Custom hook to work with slice of state conveniently
496
+ <details>
497
+ <Summary>Example</Summary>
498
+
499
+ ```typescript
500
+ 'use client';
501
+
502
+ import React from 'react';
503
+ import { useUrlState } from 'state-in-url/next';
504
+
505
+ const form: Form = {
506
+ name: '',
507
+ age: undefined,
508
+ agree_to_terms: false,
509
+ tags: [],
510
+ };
511
+
512
+ type Form = {
513
+ name: string;
514
+ age?: number;
515
+ agree_to_terms: boolean;
516
+ tags: {id: string; value: {text: string; time: Date } }[];
517
+ };
518
+
519
+ export const useFormState = ({ searchParams }: { searchParams?: object }) => {
520
+ const { urlState, setUrl: setUrlBase, reset } = useUrlState(form, {
521
+ searchParams,
522
+ });
523
+
524
+ // first navigation will push new history entry
525
+ // all following will just replace that entry
526
+ // this way will have history with only 2 entries - ['/url', '/url?key=param']
527
+
528
+ const replace = React.useRef(false);
529
+ const setUrl = React.useCallback((
530
+ state: Parameters<typeof setUrlBase>[0],
531
+ opts?: Parameters<typeof setUrlBase>[1]
532
+ ) => {
533
+ setUrlBase(state, { replace: replace.current, ...opts });
534
+ replace.current = true;
535
+ }, [setUrlBase]);
536
+
537
+ return { urlState, setUrl, resetUrl: reset };
538
+ };
539
+ ```
540
+ </details>
541
+
542
+ <hr />
543
+
544
+ ##### With complex state shape
545
+
546
+ <details>
547
+ <Summary>Example</Summary>
548
+
549
+ ```typescript
550
+ export const form: Form = {
551
+ name: '',
552
+ age: undefined,
553
+ agree_to_terms: false,
554
+ tags: [],
555
+ };
556
+
557
+ type Form = {
558
+ name: string;
559
+ age?: number;
560
+ agree_to_terms: boolean;
561
+ tags: { id: string; value: { text: string; time: Date } }[];
562
+ };
563
+ ```
564
+
565
+ ```typescript
566
+ 'use client'
567
+ import { useUrlState } from 'state-in-url/next';
568
+
569
+ import { form } from './form';
570
+
571
+ function TagsComponent() {
572
+ // `urlState` will infer from Form type!
573
+ const { urlState, setUrl } = useUrlState(form);
574
+
575
+ const onChangeTags = React.useCallback(
576
+ (tag: (typeof tags)[number]) => {
577
+ setUrl((curr) => ({
578
+ ...curr,
579
+ tags: curr.tags.find((t) => t.id === tag.id)
580
+ ? curr.tags.filter((t) => t.id !== tag.id)
581
+ : curr.tags.concat(tag),
582
+ }));
583
+ },
584
+ [setUrl],
585
+ );
586
+
587
+ return (
588
+ <div>
589
+ <Field text="Tags">
590
+ <div className="flex flex-wrap gap-2">
591
+ {tags.map((tag) => (
592
+ <Tag
593
+ active={!!urlState.tags.find((t) => t.id === tag.id)}
594
+ text={tag.value.text}
595
+ onClick={() => onChangeTags(tag)}
596
+ key={tag.id}
597
+ />
598
+ ))}
599
+ </div>
600
+ </Field>
601
+ </div>
602
+ );
603
+ }
604
+
605
+ const tags = [
606
+ {
607
+ id: '1',
608
+ value: { text: 'React.js', time: new Date('2024-07-17T04:53:17.000Z') },
609
+ },
610
+ {
611
+ id: '2',
612
+ value: { text: 'Next.js', time: new Date('2024-07-18T04:53:17.000Z') },
613
+ },
614
+ {
615
+ id: '3',
616
+ value: { text: 'TailwindCSS', time: new Date('2024-07-19T04:53:17.000Z') },
617
+ },
618
+ ];
619
+ ```
620
+
621
+ [Demo page example code](https://github.com/asmyshlyaev177/state-in-url/blob/master/packages/example-nextjs14/src/app/Form.tsx)
622
+ </details>
623
+
624
+ ##### Update state only and sync to URL manually
625
+
626
+ <details>
627
+ <Summary>Example</Summary>
628
+
629
+ ```typescript
630
+
631
+ const timer = React.useRef(0 as unknown as NodeJS.Timeout);
632
+ React.useEffect(() => {
633
+ clearTimeout(timer.current);
634
+ timer.current = setTimeout(() => {
635
+ // will compare state by content not by reference and fire update only for new values
636
+ setUrl(urlState);
637
+ }, 500);
638
+
639
+ return () => {
640
+ clearTimeout(timer.current);
641
+ };
642
+ }, [urlState, setUrl]);
643
+ ```
644
+
645
+ Syncing state `onBlur` will be more aligned with real world usage.
646
+
647
+ ```typescript
648
+ <input onBlur={() => updateUrl()} .../>
649
+ ```
650
+
651
+ </details>
652
+
632
653
  ## Other hooks and helpers
633
654
 
634
655
  ### `useUrlStateBase` hook for others routers
@@ -728,6 +749,12 @@ See [Contributing doc](CONTRIBUTING.md)
728
749
 
729
750
  ## [Changelog](CHANGELOG.md)
730
751
 
752
+ ## Mentions
753
+
754
+ - [This Week in React 209](https://thisweekinreact.com/newsletter/209)
755
+ - [JavaScript Weekly](https://javascriptweekly.com/issues/741)
756
+ - [This Week in React 240](https://thisweekinreact.com/newsletter/240)
757
+
731
758
  ## License
732
759
 
733
760
  This project is licensed under the [MIT license](LICENSE).
@@ -26,10 +26,11 @@ import { type JSONCompatible } from "../../utils";
26
26
  * setState({ name: 'test' });
27
27
  * setUrl({ name: 'test' }, { replace: true, scroll: true });
28
28
  * setUrl(curr => ({ ...curr, name: 'test' }), { replace: true, scroll: true });
29
- * // reset state and url
29
+ * // RESET state and url
30
+ * setUrl((_curr, initialState) => initialState);
31
+ * // Or
30
32
  * reset();
31
33
  * reset({ replace: true });
32
- * // same as setState(form) with setUrl(form)
33
34
  * ```
34
35
  *
35
36
  * * Docs {@link https://github.com/asmyshlyaev177/state-in-url/tree/master/packages/urlstate/next/useUrlState}
@@ -49,18 +50,20 @@ export declare function useUrlState<T extends JSONCompatible>(defaultState: T, p
49
50
  *
50
51
  * * Docs {@link https://github.com/asmyshlyaev177/state-in-url/tree/master/packages/urlstate/next/useUrlState#updatestate}
51
52
  */
52
- setState: (value: Partial<T> | ((currState: T) => T)) => void;
53
+ setState: (value: Partial<T> | ((currState: T, initial: T) => T)) => void;
53
54
  /**
54
55
  * * Example:
55
56
  * ```ts
56
57
  * setUrl({ name: 'test' });
57
58
  * // or
58
- * setUrl(curr => ({ ...curr, name: 'test' }), { replace: true, scroll: false } );
59
+ * setUrl((curr) => ({ ...curr, name: 'test' }), { replace: true, scroll: false } );
60
+ * * Reset
61
+ * setUrl((_curr, initialState) => initialState, { replace: true, scroll: false } );
59
62
  * ```
60
63
  *
61
64
  * * Docs {@link https://github.com/asmyshlyaev177/state-in-url/tree/master/packages/urlstate/next/useUrlState#updateurl}
62
65
  */
63
- setUrl: (value?: Partial<T> | ((currState: T) => T), options?: Options) => void;
66
+ setUrl: (value?: Partial<T> | ((currState: T, initialState: T) => T), options?: Options) => void;
64
67
  /**
65
68
  * * Example:
66
69
  * ```ts
@@ -23,10 +23,11 @@ import { type JSONCompatible } from "../../utils";
23
23
  * setUrl({ name: 'test' }, { replace: true });
24
24
  * // similar to React.useState
25
25
  * setUrl(curr => ({ ...curr, name: 'test' }), { replace: true });
26
- * // reset state and url
26
+ * // RESET state and url
27
+ * setUrl((_curr, initialState) => initialState);
28
+ * // Or
27
29
  * reset();
28
30
  * reset({ replace: true });
29
- * // same as setState(form) with setUrl(form)
30
31
  * ```
31
32
  *
32
33
  * * Docs {@link https://github.com/asmyshlyaev177/state-in-url/tree/master/packages/urlstate/react-router/useUrlState}
@@ -41,25 +42,25 @@ export declare function useUrlState<T extends JSONCompatible>(defaultState: T, p
41
42
  * ```ts
42
43
  * setState({ name: 'test' });
43
44
  * // or
44
- * setState(curr => ({ ...curr, name: 'test' }) );
45
+ * setState((curr, initial) => ({ ...curr, name: 'test' }) );
45
46
  * ```
46
47
  *
47
48
  * * Docs {@link https://github.com/asmyshlyaev177/state-in-url/tree/master/packages/urlstate/react-router/useUrlState#updatestate}
48
49
  */
49
- setState: (value: Partial<T> | ((currState: T) => T)) => void;
50
+ setState: (value: Partial<T> | ((currState: T, initial: T) => T)) => void;
50
51
  /**
51
52
  * * Example:
52
53
  * ```ts
53
54
  * setUrl({ name: 'test' });
54
55
  * // or
55
- * setUrl(curr => ({ ...curr, name: 'test' }), { replace: true } );
56
+ * setUrl((curr, initial) => ({ ...curr, name: 'test' }), { replace: true } );
56
57
  * // can pass optional React-Router `NavigateOptions`
57
58
  * setUrl(curr => ({ ...curr, name: 'test' }), { preventScrollReset: false } );
58
59
  * ```
59
60
  *
60
61
  * * Docs {@link https://github.com/asmyshlyaev177/state-in-url/tree/master/packages/urlstate/react-router/useUrlState#updateurl}
61
62
  */
62
- setUrl: (value?: Partial<T> | ((currState: T) => T), options?: Params) => void;
63
+ setUrl: (value?: Partial<T> | ((currState: T, initial: T) => T), options?: Params) => void;
63
64
  /**
64
65
  * * Example:
65
66
  * ```ts
@@ -23,10 +23,11 @@ import { type JSONCompatible } from "../../utils";
23
23
  * setUrl({ name: 'test' }, { replace: true });
24
24
  * // similar to React.useState
25
25
  * setUrl(curr => ({ ...curr, name: 'test' }), { replace: true });
26
- * // reset state and url
26
+ * // RESET state and url
27
+ * setUrl((_curr, initialState) => initialState);
28
+ * // Or
27
29
  * reset();
28
30
  * reset({ replace: true });
29
- * // same as setState(form) with setUrl(form)
30
31
  * ```
31
32
  *
32
33
  * * Docs {@link https://github.com/asmyshlyaev177/state-in-url/tree/master/packages/urlstate/remix/useUrlState}
@@ -41,25 +42,25 @@ export declare function useUrlState<T extends JSONCompatible>(defaultState: T, p
41
42
  * ```ts
42
43
  * setState({ name: 'test' });
43
44
  * // or
44
- * setState(curr => ({ ...curr, name: 'test' }) );
45
+ * setState((curr, initial) => ({ ...curr, name: 'test' }) );
45
46
  * ```
46
47
  *
47
48
  * * Docs {@link https://github.com/asmyshlyaev177/state-in-url/tree/master/packages/urlstate/remix/useUrlState#updatestate}
48
49
  */
49
- setState: (value: Partial<T> | ((currState: T) => T)) => void;
50
+ setState: (value: Partial<T> | ((currState: T, initial: T) => T)) => void;
50
51
  /**
51
52
  * * Example:
52
53
  * ```ts
53
54
  * setUrl({ name: 'test' });
54
55
  * // or
55
- * setUrl(curr => ({ ...curr, name: 'test' }), { replace: true } );
56
+ * setUrl((curr, initial) => ({ ...curr, name: 'test' }), { replace: true } );
56
57
  * // can pass optional React-Router `NavigateOptions`
57
58
  * setUrl(curr => ({ ...curr, name: 'test' }), { preventScrollReset: false } );
58
59
  * ```
59
60
  *
60
61
  * * Docs {@link https://github.com/asmyshlyaev177/state-in-url/tree/master/packages/urlstate/remix/useUrlState#updateurl}
61
62
  */
62
- setUrl: (value?: Partial<T> | ((currState: T) => T), options?: Params) => void;
63
+ setUrl: (value?: Partial<T> | ((currState: T, initial: T) => T), options?: Params) => void;
63
64
  /**
64
65
  * * Example:
65
66
  * ```ts
@@ -13,12 +13,12 @@ import { type JSONCompatible } from "../utils";
13
13
  *
14
14
  * setState({ name: 'test' });
15
15
  * // OR
16
- * setState(curr => ({ ...curr, name: 'test' }))
16
+ * setState((curr, initialState) => ({ ...curr, name: 'test' }))
17
17
  * ```
18
18
  * * Docs {@link https://github.com/asmyshlyaev177/state-in-url/tree/master/packages/urlstate/useSharedState}
19
19
  */
20
20
  export declare function useSharedState<T extends JSONCompatible>(defaultState: T, _getInitial?: () => T): {
21
21
  state: T;
22
22
  getState: () => T;
23
- setState: (value: Partial<T> | ((currState: T) => T)) => void;
23
+ setState: (value: Partial<T> | ((currState: T, defaultState: T) => T)) => void;
24
24
  };
@@ -1 +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;
1
+ "use strict";var b=require("react");var s=require("../subscribers.js");var M=require("../useInsertionEffect.js");var i=require("../utils.js");function g(u){return u&&u.__esModule?u:{default:u}}var n=g(b);function v(u,a){const t=n.default.useRef(u);const[o,f]=n.default.useState(()=>{if(i.isSSR){return a?a?.():t.current}const e=s.stateMap.get(t.current);if(!e){const r=a?.()||t.current;s.stateMap.set(t.current,r);return r}else{return e}});const l=n.default.useCallback(e=>{const r=s.stateMap.get(t.current);const S=typeof e==="function";const c=S?e(r,u):{...r,...e};if(i.isEqual(r,c))return void 0;s.stateMap.set(t.current,c);s.subscribers.get(t.current).forEach(p=>p())},[]);M.useInsertionEffect(()=>{const e=()=>{f(s.stateMap.get(t.current)||t.current)};const r=s.subscribers.add(t.current,e);return()=>{r()}},[]);const d=n.default.useCallback(()=>s.stateMap.get(t.current)||t.current,[]);return{state:o,getState:d,setState:l}}exports.useSharedState=v;
@@ -1 +1 @@
1
- import c from"react";import{stateMap as n,subscribers as o}from"../subscribers.mjs";import{useInsertionEffect as l}from"../useInsertionEffect.mjs";import{isSSR as b,isEqual as d}from"../utils.mjs";function E(a,u){const t=c.useRef(a);const[f,i]=c.useState(()=>{if(b){return u?u?.():t.current}const e=n.get(t.current);if(!e){const r=u?.()||t.current;n.set(t.current,r);return r}else{return e}});const m=c.useCallback(e=>{const r=n.get(t.current);const S=typeof e==="function";const s=S?e(r):{...r,...e};if(d(r,s))return void 0;n.set(t.current,s);o.get(t.current).forEach(g=>g())},[]);l(()=>{const e=()=>{i(n.get(t.current)||t.current)};const r=o.add(t.current,e);return()=>{r()}},[]);const p=c.useCallback(()=>n.get(t.current)||t.current,[]);return{state:f,getState:p,setState:m}}export{E as useSharedState};
1
+ import c from"react";import{stateMap as n,subscribers as a}from"../subscribers.mjs";import{useInsertionEffect as l}from"../useInsertionEffect.mjs";import{isSSR as b,isEqual as d}from"../utils.mjs";function E(o,s){const t=c.useRef(o);const[f,i]=c.useState(()=>{if(b){return s?s?.():t.current}const e=n.get(t.current);if(!e){const r=s?.()||t.current;n.set(t.current,r);return r}else{return e}});const m=c.useCallback(e=>{const r=n.get(t.current);const S=typeof e==="function";const u=S?e(r,o):{...r,...e};if(d(r,u))return void 0;n.set(t.current,u);a.get(t.current).forEach(g=>g())},[]);l(()=>{const e=()=>{i(n.get(t.current)||t.current)};const r=a.add(t.current,e);return()=>{r()}},[]);const p=c.useCallback(()=>n.get(t.current)||t.current,[]);return{state:f,getState:p,setState:m}}export{E as useSharedState};
@@ -23,7 +23,7 @@ import { type JSONCompatible, type Router } from "../utils";
23
23
  * );
24
24
  *
25
25
  * updateState({ name: 'John' });
26
- * updateState(curr => ({ ...curr, name: 'John' }));
26
+ * updateState((curr, initialState) => ({ ...curr, name: 'John' }));
27
27
  * updateUrl({ name: 'John' }, { replace: true });
28
28
  * updateUrl(curr => ({ ...curr, name: 'John' }), { replace: true });
29
29
  * reset()
@@ -36,8 +36,8 @@ import { type JSONCompatible, type Router } from "../utils";
36
36
  export declare function useUrlStateBase<T extends JSONCompatible>(defaultState: T, router: Router, getInitialState?: ({ parse, }: {
37
37
  parse: ReturnType<typeof useUrlEncode<T>>["parse"];
38
38
  }) => T, basename?: string): {
39
- updateState: (value: Partial<T> | ((currState: T) => T)) => void;
40
- updateUrl: (value?: Parameters<(value: Partial<T> | ((currState: T) => T)) => void>[0], options?: Options) => void;
39
+ updateState: (value: Partial<T> | ((currState: T, defaultState: T) => T)) => void;
40
+ updateUrl: (value?: Parameters<(value: Partial<T> | ((currState: T, defaultState: T) => T)) => void>[0], options?: Options) => void;
41
41
  state: T;
42
42
  reset: (options?: Options) => void;
43
43
  getState: () => T;
@@ -1 +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;
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 L=require("../utils.js");function P(e){return e&&e.__esModule?e:{default:e}}var u=P(y);function b(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(L.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(),e):{...a(),...n};const v=o(S,C(e));const m=M(window.location.pathname,l);const U=`${m}${v.length?"?":""}${v}${window.location.hash}`;const $=`${m}${window.location.search}${window.location.hash}`;if(U===$)return;r(S);const{replace:g,...k}=p||{};w.current.push([g?"replace":"push",U,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=b;
@@ -1 +1 @@
1
- import i from"react";import{TIMEOUT as T}from"../constants/constants.mjs";import{useInsertionEffect as b}from"../useInsertionEffect.mjs";import{useSharedState as v}from"../useSharedState/useSharedState.mjs";import{useUrlEncode as L}from"../useUrlEncode/useUrlEncode.mjs";import{filterUnknownParamsClient as P}from"../utils.mjs";function B(t,e,u,f){const{parse:r,stringify:s}=L(t);const{state:l,getState:c,setState:o}=v(t,()=>u?.({parse:r})||t);const w=i.useRef(void 0);b(()=>{const n=()=>{const d=r(P(t,q()));o(d)};window.addEventListener(g,n);return()=>{window.removeEventListener(g,n);clearTimeout(w.current)}},[o]);const m=i.useRef([]);const p=i.useCallback((n,d)=>{const h=typeof n==="function"?n(c()):{...c(),...n};const S=s(h,C(t));const $=I(window.location.pathname,f);const U=`${$}${S.length?"?":""}${S}${window.location.hash}`;const E=`${$}${window.location.search}${window.location.hash}`;if(U===E)return;o(h);const{replace:y,...R}=d||{};m.current.push([y?"replace":"push",U,R]);clearTimeout(w.current);const a=m.current.filter(Boolean).at(-1);if(a){queueMicrotask(()=>{w.current=setTimeout(()=>{m.current=[];e[a[0]](a[1],a[2])},T)})}},[e,s,c]);const k=i.useCallback(n=>{o(t);p(t,n)},[p,o]);return{updateState:o,updateUrl:p,state:l,reset:k,getState:c}}function C(t){const e=Object.keys(t);const u=window.location.search;const f=new URLSearchParams(u);const r=new URLSearchParams;for(const[s,l]of f){!e.includes(s)&&r.set(s,l)}return r}const g="popstate";function q(){return typeof window!=="undefined"&&window.location.search||""}function I(t,e){if(!e||e==="/"){return t}return t.slice(e.length)}export{B as useUrlStateBase};
1
+ import i from"react";import{TIMEOUT as T}from"../constants/constants.mjs";import{useInsertionEffect as v}from"../useInsertionEffect.mjs";import{useSharedState as L}from"../useSharedState/useSharedState.mjs";import{useUrlEncode as P}from"../useUrlEncode/useUrlEncode.mjs";import{filterUnknownParamsClient as b}from"../utils.mjs";function B(t,e,u,f){const{parse:r,stringify:s}=P(t);const{state:l,getState:c,setState:o}=L(t,()=>u?.({parse:r})||t);const m=i.useRef(void 0);v(()=>{const n=()=>{const d=r(b(t,O()));o(d)};window.addEventListener(g,n);return()=>{window.removeEventListener(g,n);clearTimeout(m.current)}},[o]);const w=i.useRef([]);const p=i.useCallback((n,d)=>{const h=typeof n==="function"?n(c(),t):{...c(),...n};const S=s(h,C(t));const $=q(window.location.pathname,f);const U=`${$}${S.length?"?":""}${S}${window.location.hash}`;const E=`${$}${window.location.search}${window.location.hash}`;if(U===E)return;o(h);const{replace:y,...R}=d||{};w.current.push([y?"replace":"push",U,R]);clearTimeout(m.current);const a=w.current.filter(Boolean).at(-1);if(a){queueMicrotask(()=>{m.current=setTimeout(()=>{w.current=[];e[a[0]](a[1],a[2])},T)})}},[e,s,c]);const k=i.useCallback(n=>{o(t);p(t,n)},[p,o]);return{updateState:o,updateUrl:p,state:l,reset:k,getState:c}}function C(t){const e=Object.keys(t);const u=window.location.search;const f=new URLSearchParams(u);const r=new URLSearchParams;for(const[s,l]of f){!e.includes(s)&&r.set(s,l)}return r}const g="popstate";function O(){return typeof window!=="undefined"&&window.location.search||""}function q(t,e){if(!e||e==="/"){return t}return t.slice(e.length)}export{B as useUrlStateBase};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "state-in-url",
3
- "version": "5.1.0",
3
+ "version": "5.2.0",
4
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": {