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 +207 -180
- package/dist/next/useUrlState/useUrlState.d.ts +8 -5
- package/dist/react-router/useUrlState/useUrlState.d.ts +7 -6
- package/dist/remix/useUrlState/useUrlState.d.ts +7 -6
- package/dist/useSharedState/useSharedState.d.ts +2 -2
- package/dist/useSharedState/useSharedState.js +1 -1
- package/dist/useSharedState/useSharedState.mjs +1 -1
- package/dist/useUrlStateBase/useUrlStateBase.d.ts +3 -3
- package/dist/useUrlStateBase/useUrlStateBase.js +1 -1
- package/dist/useUrlStateBase/useUrlStateBase.mjs +1 -1
- package/package.json +1 -1
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
|
-
- [
|
|
103
|
-
- [
|
|
104
|
-
- -
|
|
105
|
-
-
|
|
106
|
-
-
|
|
107
|
-
- [
|
|
108
|
-
-
|
|
109
|
-
-
|
|
110
|
-
-
|
|
111
|
-
-
|
|
112
|
-
-
|
|
113
|
-
- [
|
|
114
|
-
-
|
|
115
|
-
- [
|
|
116
|
-
-
|
|
117
|
-
-
|
|
118
|
-
-
|
|
119
|
-
-
|
|
120
|
-
- -
|
|
121
|
-
-
|
|
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
|
|
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={
|
|
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
|
-
* //
|
|
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
|
-
* //
|
|
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
|
-
* //
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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": {
|