state-in-url 5.0.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 +284 -169
- package/dist/constants/constants.js +1 -0
- package/dist/constants/index.js +1 -0
- package/dist/encodeState/encodeState.js +1 -0
- package/dist/encodeState/index.js +1 -0
- package/dist/encoder/encoder.js +1 -0
- package/dist/encoder/index.js +1 -0
- package/dist/index.js +1 -0
- package/dist/next/index.js +1 -0
- package/dist/next/useUrlState/index.js +1 -0
- package/dist/next/useUrlState/useUrlState.d.ts +8 -5
- package/dist/next/useUrlState/useUrlState.js +1 -0
- package/dist/parseSPObj.js +1 -0
- package/dist/react-router/index.js +1 -0
- package/dist/react-router/useUrlState/index.js +1 -0
- package/dist/react-router/useUrlState/useUrlState.d.ts +7 -6
- package/dist/react-router/useUrlState/useUrlState.js +1 -0
- package/dist/remix/index.js +1 -0
- package/dist/remix/useUrlState/index.js +1 -0
- package/dist/remix/useUrlState/useUrlState.d.ts +7 -6
- package/dist/remix/useUrlState/useUrlState.js +1 -0
- package/dist/subscribers.js +1 -0
- package/dist/useInsertionEffect.js +1 -0
- package/dist/useSharedState/index.js +1 -0
- package/dist/useSharedState/useSharedState.d.ts +2 -2
- package/dist/useSharedState/useSharedState.js +1 -0
- package/dist/useSharedState/useSharedState.mjs +1 -1
- package/dist/useUrlEncode/index.js +1 -0
- package/dist/useUrlEncode/useUrlEncode.js +1 -0
- package/dist/useUrlStateBase/index.js +1 -0
- package/dist/useUrlStateBase/useUrlStateBase.d.ts +3 -3
- package/dist/useUrlStateBase/useUrlStateBase.js +1 -0
- package/dist/useUrlStateBase/useUrlStateBase.mjs +1 -1
- package/dist/utils.js +1 -0
- package/package.json +23 -12
package/README.md
CHANGED
|
@@ -99,25 +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
|
-
- -
|
|
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)
|
|
121
142
|
|
|
122
143
|
## installation
|
|
123
144
|
|
|
@@ -174,7 +195,7 @@ import { userState } from './userState';
|
|
|
174
195
|
function MyComponent() {
|
|
175
196
|
// can pass `replace` arg, it's control will `setUrl` will use `rounter.push` or `router.replace`, default replace=true
|
|
176
197
|
// can pass `searchParams` from server components, pass `useHistory: false` if you need to fetch smt in the server component
|
|
177
|
-
const { urlState, setUrl, setState
|
|
198
|
+
const { urlState, setUrl, setState } = useUrlState(userState);
|
|
178
199
|
|
|
179
200
|
return (
|
|
180
201
|
<div>
|
|
@@ -193,7 +214,7 @@ function MyComponent() {
|
|
|
193
214
|
onBlur={() => setUrl()}
|
|
194
215
|
/>
|
|
195
216
|
|
|
196
|
-
<button onClick={
|
|
217
|
+
<button onClick={() => setUrl((_, initial) => initial)}>
|
|
197
218
|
Reset
|
|
198
219
|
</button>
|
|
199
220
|
|
|
@@ -202,60 +223,108 @@ function MyComponent() {
|
|
|
202
223
|
}
|
|
203
224
|
```
|
|
204
225
|
|
|
205
|
-
#####
|
|
226
|
+
##### With server side rendering
|
|
227
|
+
|
|
206
228
|
<details>
|
|
207
229
|
<Summary>Example</Summary>
|
|
208
230
|
|
|
209
|
-
|
|
210
|
-
|
|
231
|
+
```typescript
|
|
232
|
+
export default async function Home({ searchParams }: { searchParams: object }) {
|
|
233
|
+
return (
|
|
234
|
+
<Form searchParams={searchParams} />
|
|
235
|
+
)
|
|
236
|
+
}
|
|
211
237
|
|
|
238
|
+
// Form.tsx
|
|
239
|
+
'use client'
|
|
212
240
|
import React from 'react';
|
|
213
241
|
import { useUrlState } from 'state-in-url/next';
|
|
242
|
+
import { form } from './form';
|
|
214
243
|
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
tags: [],
|
|
220
|
-
};
|
|
244
|
+
const Form = ({ searchParams }: { searchParams: object }) => {
|
|
245
|
+
const { urlState, setState, setUrl } = useUrlState(form, { searchParams });
|
|
246
|
+
}
|
|
247
|
+
```
|
|
221
248
|
|
|
222
|
-
|
|
223
|
-
name: string;
|
|
224
|
-
age?: number;
|
|
225
|
-
agree_to_terms: boolean;
|
|
226
|
-
tags: {id: string; value: {text: string; time: Date } }[];
|
|
227
|
-
};
|
|
249
|
+
</details>
|
|
228
250
|
|
|
229
|
-
|
|
230
|
-
const { urlState, setUrl: setUrlBase, reset } = useUrlState(form, {
|
|
231
|
-
searchParams,
|
|
232
|
-
});
|
|
251
|
+
##### Using hook in `layout` component
|
|
233
252
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
253
|
+
<details>
|
|
254
|
+
<Summary>Example</Summary>
|
|
255
|
+
That a tricky part, since nextjs with app router doesn't allow to access searchParams from server side. There is workaround with using middleware, but it isn't pretty and can stop working after nextjs update.
|
|
237
256
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
opts?: Parameters<typeof setUrlBase>[1]
|
|
242
|
-
) => {
|
|
243
|
-
setUrlBase(state, { replace: replace.current, ...opts });
|
|
244
|
-
replace.current = true;
|
|
245
|
-
}, [setUrlBase]);
|
|
257
|
+
```typescript
|
|
258
|
+
// add to appropriate `layout.tsc`
|
|
259
|
+
export const runtime = 'edge';
|
|
246
260
|
|
|
247
|
-
|
|
248
|
-
};
|
|
249
|
-
|
|
250
|
-
</details>
|
|
261
|
+
// middleware.ts
|
|
262
|
+
import type { NextRequest } from 'next/server';
|
|
263
|
+
import { NextResponse } from 'next/server';
|
|
251
264
|
|
|
252
|
-
|
|
265
|
+
export function middleware(request: NextRequest) {
|
|
266
|
+
const url = request.url?.includes('_next') ? null : request.url;
|
|
267
|
+
const sp = url?.split?.('?')?.[1] || '';
|
|
253
268
|
|
|
254
|
-
|
|
269
|
+
const response = NextResponse.next();
|
|
270
|
+
|
|
271
|
+
if (url !== null) {
|
|
272
|
+
response.headers.set('searchParams', sp);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return response;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Target layout component
|
|
279
|
+
import { headers } from 'next/headers';
|
|
280
|
+
import { decodeState } from 'state-in-url/encodeState';
|
|
281
|
+
|
|
282
|
+
export default async function Layout({
|
|
283
|
+
children,
|
|
284
|
+
}: {
|
|
285
|
+
children: React.ReactNode;
|
|
286
|
+
}) {
|
|
287
|
+
const sp = headers().get('searchParams') || '';
|
|
288
|
+
|
|
289
|
+
return (
|
|
290
|
+
<div>
|
|
291
|
+
<Comp1 searchParams={decodeState(sp, stateShape)} />
|
|
292
|
+
{children}
|
|
293
|
+
</div>
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
</details>
|
|
301
|
+
|
|
302
|
+
##### With arbitrary state shape (not recommended)
|
|
255
303
|
|
|
256
304
|
<details>
|
|
257
305
|
<Summary>Example</Summary>
|
|
258
306
|
|
|
307
|
+
```typescript
|
|
308
|
+
'use client'
|
|
309
|
+
import { useUrlState } from 'state-in-url/next';
|
|
310
|
+
|
|
311
|
+
const someObj = {};
|
|
312
|
+
|
|
313
|
+
function SettingsComponent() {
|
|
314
|
+
const { urlState, setUrl, setState } = useUrlState<object>(someObj);
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
</details>
|
|
319
|
+
|
|
320
|
+
### useUrlState hook for Remix.js
|
|
321
|
+
|
|
322
|
+
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.
|
|
323
|
+
|
|
324
|
+
[API Docs](packages/urlstate/remix/useUrlState)
|
|
325
|
+
|
|
326
|
+
#### Example
|
|
327
|
+
|
|
259
328
|
```typescript
|
|
260
329
|
export const form: Form = {
|
|
261
330
|
name: '',
|
|
@@ -270,17 +339,16 @@ type Form = {
|
|
|
270
339
|
agree_to_terms: boolean;
|
|
271
340
|
tags: { id: string; value: { text: string; time: Date } }[];
|
|
272
341
|
};
|
|
342
|
+
|
|
273
343
|
```
|
|
274
344
|
|
|
275
345
|
```typescript
|
|
276
|
-
|
|
277
|
-
import { useUrlState } from 'state-in-url/next';
|
|
346
|
+
import { useUrlState } from 'state-in-url/remix';
|
|
278
347
|
|
|
279
348
|
import { form } from './form';
|
|
280
349
|
|
|
281
350
|
function TagsComponent() {
|
|
282
|
-
|
|
283
|
-
const { urlState, setUrl } = useUrlState(form);
|
|
351
|
+
const { urlState, setUrl, setState } = useUrlState(form);
|
|
284
352
|
|
|
285
353
|
const onChangeTags = React.useCallback(
|
|
286
354
|
(tag: (typeof tags)[number]) => {
|
|
@@ -308,6 +376,12 @@ function TagsComponent() {
|
|
|
308
376
|
))}
|
|
309
377
|
</div>
|
|
310
378
|
</Field>
|
|
379
|
+
|
|
380
|
+
<input value={urlState.name}
|
|
381
|
+
onChange={(ev) => { setState(curr => ({ ...curr, name: ev.target.value })) }}
|
|
382
|
+
// Can update state immediately but sync change to url as needed
|
|
383
|
+
onBlur={() => setUrl()}
|
|
384
|
+
/>
|
|
311
385
|
</div>
|
|
312
386
|
);
|
|
313
387
|
}
|
|
@@ -328,139 +402,149 @@ const tags = [
|
|
|
328
402
|
];
|
|
329
403
|
```
|
|
330
404
|
|
|
331
|
-
[
|
|
332
|
-
</details>
|
|
333
|
-
|
|
334
|
-
##### Update state only and sync to URL manually
|
|
335
|
-
|
|
336
|
-
<details>
|
|
337
|
-
<Summary>Example</Summary>
|
|
338
|
-
|
|
339
|
-
```typescript
|
|
405
|
+
[Example code](packages/example-remix2/app/routes/Form-for-test.tsx)
|
|
340
406
|
|
|
341
|
-
|
|
342
|
-
React.useEffect(() => {
|
|
343
|
-
clearTimeout(timer.current);
|
|
344
|
-
timer.current = setTimeout(() => {
|
|
345
|
-
// will compare state by content not by reference and fire update only for new values
|
|
346
|
-
setUrl(urlState);
|
|
347
|
-
}, 500);
|
|
348
|
-
|
|
349
|
-
return () => {
|
|
350
|
-
clearTimeout(timer.current);
|
|
351
|
-
};
|
|
352
|
-
}, [urlState, setUrl]);
|
|
353
|
-
```
|
|
354
|
-
|
|
355
|
-
Syncing state `onBlur` will be more aligned with real world usage.
|
|
356
|
-
|
|
357
|
-
```typescript
|
|
358
|
-
<input onBlur={() => updateUrl()} .../>
|
|
359
|
-
```
|
|
407
|
+
### useUrlState hook for React-Router
|
|
360
408
|
|
|
361
|
-
|
|
409
|
+
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.
|
|
362
410
|
|
|
363
|
-
|
|
411
|
+
[API Docs](packages/urlstate/react-router/useUrlState)
|
|
364
412
|
|
|
365
|
-
|
|
366
|
-
<Summary>Example</Summary>
|
|
413
|
+
#### Example
|
|
367
414
|
|
|
368
415
|
```typescript
|
|
369
|
-
export
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
416
|
+
export const form: Form = {
|
|
417
|
+
name: '',
|
|
418
|
+
age: undefined,
|
|
419
|
+
agree_to_terms: false,
|
|
420
|
+
tags: [],
|
|
421
|
+
};
|
|
374
422
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
423
|
+
type Form = {
|
|
424
|
+
name: string;
|
|
425
|
+
age?: number;
|
|
426
|
+
agree_to_terms: boolean;
|
|
427
|
+
tags: { id: string; value: { text: string; time: Date } }[];
|
|
428
|
+
};
|
|
380
429
|
|
|
381
|
-
const Form = ({ searchParams }: { searchParams: object }) => {
|
|
382
|
-
const { urlState, setState, setUrl } = useUrlState(form, { searchParams });
|
|
383
|
-
}
|
|
384
430
|
```
|
|
385
431
|
|
|
386
|
-
</details>
|
|
387
|
-
|
|
388
|
-
##### Using hook in `layout` component
|
|
389
|
-
|
|
390
|
-
<details>
|
|
391
|
-
<Summary>Example</Summary>
|
|
392
|
-
That a tricky part, since nextjs with app router doesn't allow to access searchParams from server side. There is workaround with using middleware, but it isn't pretty and can stop working after nextjs update.
|
|
393
|
-
|
|
394
432
|
```typescript
|
|
395
|
-
|
|
396
|
-
export const runtime = 'edge';
|
|
397
|
-
|
|
398
|
-
// middleware.ts
|
|
399
|
-
import type { NextRequest } from 'next/server';
|
|
400
|
-
import { NextResponse } from 'next/server';
|
|
401
|
-
|
|
402
|
-
export function middleware(request: NextRequest) {
|
|
403
|
-
const url = request.url?.includes('_next') ? null : request.url;
|
|
404
|
-
const sp = url?.split?.('?')?.[1] || '';
|
|
405
|
-
|
|
406
|
-
const response = NextResponse.next();
|
|
407
|
-
|
|
408
|
-
if (url !== null) {
|
|
409
|
-
response.headers.set('searchParams', sp);
|
|
410
|
-
}
|
|
433
|
+
import { useUrlState } from 'state-in-url/react-router';
|
|
411
434
|
|
|
412
|
-
|
|
413
|
-
}
|
|
435
|
+
import { form } from './form';
|
|
414
436
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
import { decodeState } from 'state-in-url/encodeState';
|
|
437
|
+
function TagsComponent() {
|
|
438
|
+
const { urlState, setUrl, setState } = useUrlState(form);
|
|
418
439
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
440
|
+
const onChangeTags = React.useCallback(
|
|
441
|
+
(tag: (typeof tags)[number]) => {
|
|
442
|
+
setUrl((curr) => ({
|
|
443
|
+
...curr,
|
|
444
|
+
tags: curr.tags.find((t) => t.id === tag.id)
|
|
445
|
+
? curr.tags.filter((t) => t.id !== tag.id)
|
|
446
|
+
: curr.tags.concat(tag),
|
|
447
|
+
}));
|
|
448
|
+
},
|
|
449
|
+
[setUrl],
|
|
450
|
+
);
|
|
425
451
|
|
|
426
452
|
return (
|
|
427
453
|
<div>
|
|
428
|
-
<
|
|
429
|
-
|
|
454
|
+
<Field text="Tags">
|
|
455
|
+
<div className="flex flex-wrap gap-2">
|
|
456
|
+
{tags.map((tag) => (
|
|
457
|
+
<Tag
|
|
458
|
+
active={!!urlState.tags.find((t) => t.id === tag.id)}
|
|
459
|
+
text={tag.value.text}
|
|
460
|
+
onClick={() => onChangeTags(tag)}
|
|
461
|
+
key={tag.id}
|
|
462
|
+
/>
|
|
463
|
+
))}
|
|
464
|
+
</div>
|
|
465
|
+
</Field>
|
|
466
|
+
|
|
467
|
+
<input value={urlState.name}
|
|
468
|
+
onChange={(ev) => { setState(curr => ({ ...curr, name: ev.target.value })) }}
|
|
469
|
+
// Can update state immediately but sync change to url as needed
|
|
470
|
+
onBlur={() => setUrl()}
|
|
471
|
+
/>
|
|
430
472
|
</div>
|
|
431
473
|
);
|
|
432
474
|
}
|
|
433
475
|
|
|
434
|
-
|
|
476
|
+
const tags = [
|
|
477
|
+
{
|
|
478
|
+
id: '1',
|
|
479
|
+
value: { text: 'React.js', time: new Date('2024-07-17T04:53:17.000Z') },
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
id: '2',
|
|
483
|
+
value: { text: 'Next.js', time: new Date('2024-07-18T04:53:17.000Z') },
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
id: '3',
|
|
487
|
+
value: { text: 'TailwindCSS', time: new Date('2024-07-19T04:53:17.000Z') },
|
|
488
|
+
},
|
|
489
|
+
];
|
|
435
490
|
```
|
|
436
491
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
##### With arbitrary state shape (not recommended)
|
|
492
|
+
[Example code](packages/example-react-router6/src/Form-for-test.tsx)
|
|
440
493
|
|
|
494
|
+
## Recipes
|
|
495
|
+
##### Custom hook to work with slice of state conveniently
|
|
441
496
|
<details>
|
|
442
497
|
<Summary>Example</Summary>
|
|
443
498
|
|
|
444
|
-
```typescript
|
|
445
|
-
'use client'
|
|
499
|
+
```typescript
|
|
500
|
+
'use client';
|
|
501
|
+
|
|
502
|
+
import React from 'react';
|
|
446
503
|
import { useUrlState } from 'state-in-url/next';
|
|
447
504
|
|
|
448
|
-
const
|
|
505
|
+
const form: Form = {
|
|
506
|
+
name: '',
|
|
507
|
+
age: undefined,
|
|
508
|
+
agree_to_terms: false,
|
|
509
|
+
tags: [],
|
|
510
|
+
};
|
|
449
511
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
512
|
+
type Form = {
|
|
513
|
+
name: string;
|
|
514
|
+
age?: number;
|
|
515
|
+
agree_to_terms: boolean;
|
|
516
|
+
tags: {id: string; value: {text: string; time: Date } }[];
|
|
517
|
+
};
|
|
454
518
|
|
|
455
|
-
|
|
519
|
+
export const useFormState = ({ searchParams }: { searchParams?: object }) => {
|
|
520
|
+
const { urlState, setUrl: setUrlBase, reset } = useUrlState(form, {
|
|
521
|
+
searchParams,
|
|
522
|
+
});
|
|
456
523
|
|
|
457
|
-
|
|
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']
|
|
458
527
|
|
|
459
|
-
|
|
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]);
|
|
460
536
|
|
|
461
|
-
|
|
537
|
+
return { urlState, setUrl, resetUrl: reset };
|
|
538
|
+
};
|
|
539
|
+
```
|
|
540
|
+
</details>
|
|
462
541
|
|
|
463
|
-
|
|
542
|
+
<hr />
|
|
543
|
+
|
|
544
|
+
##### With complex state shape
|
|
545
|
+
|
|
546
|
+
<details>
|
|
547
|
+
<Summary>Example</Summary>
|
|
464
548
|
|
|
465
549
|
```typescript
|
|
466
550
|
export const form: Form = {
|
|
@@ -476,16 +560,17 @@ type Form = {
|
|
|
476
560
|
agree_to_terms: boolean;
|
|
477
561
|
tags: { id: string; value: { text: string; time: Date } }[];
|
|
478
562
|
};
|
|
479
|
-
|
|
480
563
|
```
|
|
481
564
|
|
|
482
565
|
```typescript
|
|
483
|
-
|
|
566
|
+
'use client'
|
|
567
|
+
import { useUrlState } from 'state-in-url/next';
|
|
484
568
|
|
|
485
569
|
import { form } from './form';
|
|
486
570
|
|
|
487
571
|
function TagsComponent() {
|
|
488
|
-
|
|
572
|
+
// `urlState` will infer from Form type!
|
|
573
|
+
const { urlState, setUrl } = useUrlState(form);
|
|
489
574
|
|
|
490
575
|
const onChangeTags = React.useCallback(
|
|
491
576
|
(tag: (typeof tags)[number]) => {
|
|
@@ -513,12 +598,6 @@ function TagsComponent() {
|
|
|
513
598
|
))}
|
|
514
599
|
</div>
|
|
515
600
|
</Field>
|
|
516
|
-
|
|
517
|
-
<input value={urlState.name}
|
|
518
|
-
onChange={(ev) => { setState(curr => ({ ...curr, name: ev.target.value })) }}
|
|
519
|
-
// Can update state immediately but sync change to url as needed
|
|
520
|
-
onBlur={() => setUrl()}
|
|
521
|
-
/>
|
|
522
601
|
</div>
|
|
523
602
|
);
|
|
524
603
|
}
|
|
@@ -539,7 +618,37 @@ const tags = [
|
|
|
539
618
|
];
|
|
540
619
|
```
|
|
541
620
|
|
|
542
|
-
[
|
|
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>
|
|
543
652
|
|
|
544
653
|
## Other hooks and helpers
|
|
545
654
|
|
|
@@ -640,6 +749,12 @@ See [Contributing doc](CONTRIBUTING.md)
|
|
|
640
749
|
|
|
641
750
|
## [Changelog](CHANGELOG.md)
|
|
642
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
|
+
|
|
643
758
|
## License
|
|
644
759
|
|
|
645
760
|
This project is licensed under the [MIT license](LICENSE).
|
|
@@ -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;
|
|
@@ -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
|
|
@@ -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=_;
|
|
@@ -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;
|
|
@@ -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
|
|
@@ -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;
|
|
@@ -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;
|
|
@@ -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
|
|
@@ -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
|
+
"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;
|
|
@@ -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
|
};
|
|
@@ -0,0 +1 @@
|
|
|
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};
|
|
@@ -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;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var e=require("./useUrlStateBase.js");exports.useUrlStateBase=e.useUrlStateBase;
|
|
@@ -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;
|
|
@@ -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 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/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,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": {
|
|
@@ -36,47 +36,58 @@
|
|
|
36
36
|
"files": [
|
|
37
37
|
"dist"
|
|
38
38
|
],
|
|
39
|
-
"main": "dist/index.
|
|
39
|
+
"main": "./dist/index.js",
|
|
40
|
+
"module": "dist/index.mjs",
|
|
40
41
|
"exports": {
|
|
41
42
|
".": {
|
|
42
43
|
"types": "./dist/index.d.ts",
|
|
43
|
-
"default": "./dist/index.mjs"
|
|
44
|
+
"default": "./dist/index.mjs",
|
|
45
|
+
"require": "./dist/index.js"
|
|
44
46
|
},
|
|
45
47
|
"./next": {
|
|
46
48
|
"types": "./dist/next/index.d.ts",
|
|
47
|
-
"default": "./dist/next/index.mjs"
|
|
49
|
+
"default": "./dist/next/index.mjs",
|
|
50
|
+
"require": "./dist/next/index.js"
|
|
48
51
|
},
|
|
49
52
|
"./react-router": {
|
|
50
53
|
"types": "./dist/react-router/index.d.ts",
|
|
51
|
-
"default": "./dist/react-router/index.mjs"
|
|
54
|
+
"default": "./dist/react-router/index.mjs",
|
|
55
|
+
"require": "./dist/react-router/index.js"
|
|
52
56
|
},
|
|
53
57
|
"./remix": {
|
|
54
58
|
"types": "./dist/remix/index.d.ts",
|
|
55
|
-
"default": "./dist/remix/index.mjs"
|
|
59
|
+
"default": "./dist/remix/index.mjs",
|
|
60
|
+
"require": "./dist/remix/index.js"
|
|
56
61
|
},
|
|
57
62
|
"./encodeState": {
|
|
58
63
|
"types": "./dist/encodeState/index.d.ts",
|
|
59
|
-
"default": "./dist/encodeState/index.mjs"
|
|
64
|
+
"default": "./dist/encodeState/index.mjs",
|
|
65
|
+
"require": "./dist/encodeState/index.js"
|
|
60
66
|
},
|
|
61
67
|
"./encoder": {
|
|
62
68
|
"types": "./dist/encoder/index.d.ts",
|
|
63
|
-
"default": "./dist/encoder/index.mjs"
|
|
69
|
+
"default": "./dist/encoder/index.mjs",
|
|
70
|
+
"require": "./dist/encoder/index.js"
|
|
64
71
|
},
|
|
65
72
|
"./useSharedState": {
|
|
66
73
|
"types": "./dist/useSharedState/index.d.ts",
|
|
67
|
-
"default": "./dist/useSharedState/index.mjs"
|
|
74
|
+
"default": "./dist/useSharedState/index.mjs",
|
|
75
|
+
"require": "./dist/useSharedState/index.js"
|
|
68
76
|
},
|
|
69
77
|
"./useUrlEncode": {
|
|
70
78
|
"types": "./dist/useUrlEncode/index.d.ts",
|
|
71
|
-
"default": "./dist/useUrlEncode/index.mjs"
|
|
79
|
+
"default": "./dist/useUrlEncode/index.mjs",
|
|
80
|
+
"require": "./dist/useUrlEncode/index.js"
|
|
72
81
|
},
|
|
73
82
|
"./useUrlStateBase": {
|
|
74
83
|
"types": "./dist/useUrlStateBase/index.d.ts",
|
|
75
|
-
"default": "./dist/useUrlStateBase/index.mjs"
|
|
84
|
+
"default": "./dist/useUrlStateBase/index.mjs",
|
|
85
|
+
"require": "./dist/useUrlStateBase/index.js"
|
|
76
86
|
},
|
|
77
87
|
"./utils": {
|
|
78
88
|
"types": "./dist/utils.d.ts",
|
|
79
|
-
"default": "./dist/utils.mjs"
|
|
89
|
+
"default": "./dist/utils.mjs",
|
|
90
|
+
"require": "./dist/utils.js"
|
|
80
91
|
}
|
|
81
92
|
},
|
|
82
93
|
"types": "./dist/index.d.ts",
|