state-in-url 0.2.6 → 0.2.7
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 +29 -77
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/urlstate/constants/index.d.ts +1 -0
- package/dist/urlstate/{encodeState.d.ts → encodeState/encodeState.d.ts} +3 -3
- package/dist/urlstate/encodeState/index.d.ts +1 -0
- package/dist/urlstate/{encoder.d.ts → encoder/encoder.d.ts} +3 -3
- package/dist/urlstate/encoder/index.d.ts +1 -0
- package/dist/urlstate/index.d.ts +5 -5
- package/dist/urlstate/state.d.ts +1 -1
- package/dist/urlstate/useUrlEncode/index.d.ts +1 -0
- package/dist/urlstate/{useUrlEncode.d.ts → useUrlEncode/useUrlEncode.d.ts} +22 -2
- package/dist/urlstate/useUrlState/index.d.ts +1 -0
- package/dist/urlstate/{useUrlState.d.ts → useUrlState/useUrlState.d.ts} +23 -3
- package/dist/urlstate/utils.d.ts +1 -1
- package/package.json +10 -2
- /package/dist/urlstate/{constants.d.ts → constants/constants.d.ts} +0 -0
package/README.md
CHANGED
|
@@ -5,15 +5,13 @@
|
|
|
5
5
|
<div align="center">
|
|
6
6
|
<img src="/assets/logo.svg?raw=true" alt="Library logo"/>
|
|
7
7
|
|
|
8
|
-
<div>
|
|
8
|
+
<div>Easily share state between unrelated React components, with IDE autocomplete and TS validation. Without any hasssle or boilerplate. </div>
|
|
9
9
|
</div>
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
<div align="center">
|
|
13
13
|
<hr />
|
|
14
14
|
|
|
15
|
-
[](https://www.linkedin.com/in/asmyshlyaev177/)
|
|
16
|
-
|
|
17
15
|
|
|
18
16
|
[](https://www.npmjs.com/package/state-in-url)
|
|
19
17
|

|
|
@@ -24,12 +22,14 @@
|
|
|
24
22
|
|
|
25
23
|
<div align="center">
|
|
26
24
|
|
|
27
|
-
<h4 align="center">Don't hesitate to open issue if you found a bug</h4>
|
|
25
|
+
<h4 align="center">Don't hesitate to open an issue if you found a bug</h4>
|
|
28
26
|
|
|
29
27
|

|
|
30
28
|
|
|
31
29
|
|
|
32
|
-
<a href="https://state-in-url-asmyshlyaev177.vercel.app/" target="_blank">DEMO</a>
|
|
30
|
+
<a href="https://state-in-url-asmyshlyaev177.vercel.app/" target="_blank">DEMO</a> |
|
|
31
|
+
<a href="https://stackblitz.com/~/github.com/asmyshlyaev177/state-in-url?file=packages%2Fexample-nextjs%2Fsrc%2Fapp%2FForm.tsx" target="_blank">Demo on Stackblitz</a> |
|
|
32
|
+
<a href ="https://codesandbox.io/p/github/asmyshlyaev177/state-in-url/main?layout=%257B%2522sidebarPanel%2522%253A%2522EXPLORER%2522%252C%2522rootPanelGroup%2522%253A%257B%2522direction%2522%253A%2522horizontal%2522%252C%2522contentType%2522%253A%2522UNKNOWN%2522%252C%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522id%2522%253A%2522ROOT_LAYOUT%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522UNKNOWN%2522%252C%2522direction%2522%253A%2522vertical%2522%252C%2522id%2522%253A%2522clyk5bd9y00062v6jspcfrkx7%2522%252C%2522sizes%2522%253A%255B100%252C0%255D%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522EDITOR%2522%252C%2522direction%2522%253A%2522horizontal%2522%252C%2522id%2522%253A%2522EDITOR%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL%2522%252C%2522contentType%2522%253A%2522EDITOR%2522%252C%2522id%2522%253A%2522clyk5bd9x00022v6jyg71cr9e%2522%257D%255D%257D%252C%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522SHELLS%2522%252C%2522direction%2522%253A%2522horizontal%2522%252C%2522id%2522%253A%2522SHELLS%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL%2522%252C%2522contentType%2522%253A%2522SHELLS%2522%252C%2522id%2522%253A%2522clyk5bd9x00042v6jsos2y043%2522%257D%255D%257D%255D%257D%252C%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522DEVTOOLS%2522%252C%2522direction%2522%253A%2522vertical%2522%252C%2522id%2522%253A%2522DEVTOOLS%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL%2522%252C%2522contentType%2522%253A%2522DEVTOOLS%2522%252C%2522id%2522%253A%2522clyk5bd9x00052v6j5r632b12%2522%257D%255D%257D%255D%252C%2522sizes%2522%253A%255B50%252C50%255D%257D%252C%2522tabbedPanels%2522%253A%257B%2522clyk5bd9x00022v6jyg71cr9e%2522%253A%257B%2522tabs%2522%253A%255B%257B%2522id%2522%253A%2522clyk5bd9w00012v6j83rq3bvo%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522type%2522%253A%2522FILE%2522%252C%2522filepath%2522%253A%2522%252FREADME.md%2522%252C%2522state%2522%253A%2522IDLE%2522%257D%255D%252C%2522id%2522%253A%2522clyk5bd9x00022v6jyg71cr9e%2522%252C%2522activeTabId%2522%253A%2522clyk5bd9w00012v6j83rq3bvo%2522%257D%252C%2522clyk5bd9x00052v6j5r632b12%2522%253A%257B%2522id%2522%253A%2522clyk5bd9x00052v6j5r632b12%2522%252C%2522activeTabId%2522%253A%2522clyk5bdjx000b2v6jfc8ae464%2522%252C%2522tabs%2522%253A%255B%257B%2522type%2522%253A%2522SETUP_TASKS%2522%252C%2522id%2522%253A%2522clyk5bdjx000b2v6jfc8ae464%2522%252C%2522mode%2522%253A%2522permanent%2522%257D%252C%257B%2522type%2522%253A%2522UNASSIGNED_PORT%2522%252C%2522port%2522%253A2222%252C%2522id%2522%253A%2522clyk5cjbo004d2v6j3u55k74g%2522%252C%2522mode%2522%253A%2522permanent%2522%257D%252C%257B%2522type%2522%253A%2522ENV_SETUP%2522%252C%2522id%2522%253A%2522clyk5h8dp000r2v6j0r7kc7qq%2522%252C%2522mode%2522%253A%2522permanent%2522%257D%255D%257D%252C%2522clyk5bd9x00042v6jsos2y043%2522%253A%257B%2522id%2522%253A%2522clyk5bd9x00042v6jsos2y043%2522%252C%2522activeTabId%2522%253A%2522clyk5cbuv001q2v6joegrxxv6%2522%252C%2522tabs%2522%253A%255B%257B%2522id%2522%253A%2522clyk5bd9x00032v6jtfhj316o%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522type%2522%253A%2522NEW_TERMINAL%2522%257D%252C%257B%2522type%2522%253A%2522TASK_LOG%2522%252C%2522taskId%2522%253A%2522dev%2522%252C%2522id%2522%253A%2522clyk5cbuv001q2v6joegrxxv6%2522%252C%2522mode%2522%253A%2522permanent%2522%257D%252C%257B%2522type%2522%253A%2522TASK_LOG%2522%252C%2522taskId%2522%253A%2522cleanupDist%2522%252C%2522id%2522%253A%2522clyk5dgvo005i2v6jj950sxft%2522%252C%2522mode%2522%253A%2522permanent%2522%257D%255D%257D%257D%252C%2522showDevtools%2522%253Atrue%252C%2522showShells%2522%253Atrue%252C%2522showSidebar%2522%253Atrue%252C%2522sidebarPanelSize%2522%253A15%257D" target="_blank">Demo on Codesandbox</a>
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
<hr />
|
|
@@ -42,17 +42,19 @@ Add a ⭐️ to support the project!
|
|
|
42
42
|
|
|
43
43
|
## Why use state-in-url?
|
|
44
44
|
|
|
45
|
-
`state-in-url`
|
|
45
|
+
`state-in-url` Simple state management with URL synchronization.
|
|
46
46
|
|
|
47
47
|
# Use cases
|
|
48
|
+
|
|
49
|
+
- 🙃 Share the state between different components without changing url, good as alternative to signals and other state management tools
|
|
48
50
|
- 🔗 Shareable URLs with full application state
|
|
49
51
|
- 🔄 Easy state persistence across page reloads
|
|
50
|
-
- 🧠
|
|
51
|
-
- 🙃 Can just share the state without changing url
|
|
52
|
+
- 🧠 Sync data between unrelated client components
|
|
52
53
|
- 🧮 Store unsaved user forms in URL
|
|
53
54
|
|
|
54
55
|
# Features
|
|
55
|
-
|
|
56
|
+
|
|
57
|
+
- 🧩 **Simple**: No providers, reducers, boilerplate or new concepts
|
|
56
58
|
- 📘 **Typescript support and type Safety**: Preserves data types and structure, enhances developer experience with IDE suggestions, strong typing and JSDoc comments
|
|
57
59
|
- ⚛️ **Framework Flexibility**: Separate hooks for Next.js and React.js applications, and functions for pure JS
|
|
58
60
|
- ⚙ **Well tested**: Unit tests and Playwright tests
|
|
@@ -61,6 +63,7 @@ Add a ⭐️ to support the project!
|
|
|
61
63
|
|
|
62
64
|
|
|
63
65
|
## Table of content
|
|
66
|
+
|
|
64
67
|
- [Installation](#installation)
|
|
65
68
|
- [`useUrlState` for Next.js](#useurlstate-hook-for-nextjs)
|
|
66
69
|
- [`useUrlEncode` for React.js](#useurlencode-hook-for-reactjs)
|
|
@@ -75,7 +78,9 @@ Add a ⭐️ to support the project!
|
|
|
75
78
|
- [Inspiration](#inspiration)
|
|
76
79
|
|
|
77
80
|
## installation
|
|
81
|
+
|
|
78
82
|
### 1. Install package
|
|
83
|
+
|
|
79
84
|
```sh
|
|
80
85
|
# npm
|
|
81
86
|
npm install --save state-in-url
|
|
@@ -84,20 +89,23 @@ yarn add state-in-url
|
|
|
84
89
|
# pnpm
|
|
85
90
|
pnpm add state-in-url
|
|
86
91
|
```
|
|
92
|
+
|
|
87
93
|
### 2. Edit tsconfig.json
|
|
88
|
-
set `"moduleResolution": "Node16"` or `"moduleResolution": "NodeNext"` in your `tsconfig.json`
|
|
89
94
|
|
|
95
|
+
set `"moduleResolution": "Node16"` or `"moduleResolution": "NodeNext"` in your `tsconfig.json`
|
|
90
96
|
|
|
91
97
|
## useUrlState hook for Next.js
|
|
92
98
|
|
|
93
|
-
|
|
99
|
+
[Docs](packages/urlstate/useUrlState/README.md)
|
|
94
100
|
|
|
101
|
+
`useUrlState` is a custom React hook for Next.js applications that make communication between client components easy. It allows you to share any complex state and sync it with the URL search parameters, providing a way to persist state across page reloads and share application state via URLs.
|
|
95
102
|
|
|
96
103
|
### Usage examples
|
|
97
104
|
|
|
98
105
|
#### Basic
|
|
99
106
|
|
|
100
107
|
1. Define state shape
|
|
108
|
+
|
|
101
109
|
```typescript
|
|
102
110
|
// countState.ts
|
|
103
111
|
// State shape should be stored in a constant, don't pass an object directly
|
|
@@ -107,6 +115,7 @@ set `"moduleResolution": "Node16"` or `"moduleResolution": "NodeNext"` in your `
|
|
|
107
115
|
```
|
|
108
116
|
|
|
109
117
|
2. Import it and use
|
|
118
|
+
|
|
110
119
|
```typescript
|
|
111
120
|
'use client'
|
|
112
121
|
import { useUrlState } from 'state-in-url';
|
|
@@ -248,79 +257,17 @@ function SettingsComponent() {
|
|
|
248
257
|
}
|
|
249
258
|
```
|
|
250
259
|
|
|
251
|
-
## useUrlEncode hook for React.js
|
|
252
|
-
|
|
253
|
-
`useUrlEncode` is a custom React hook that provides utility functions for encoding and decoding state object to and from URL search parameters. This hook doesn't depend on Nextjs, and will works with any React application.
|
|
254
|
-
|
|
255
|
-
Accepts optional `defaultState` argument.
|
|
256
|
-
|
|
257
|
-
```typescript
|
|
258
|
-
import { useUrlEncode } from 'state-in-url';
|
|
260
|
+
## `useUrlEncode` hook for React.js
|
|
259
261
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
const Component = () => {
|
|
263
|
-
const { parse, stringify } = useUrlEncode();
|
|
264
|
-
|
|
265
|
-
const str = stringify({ age: 36 }); // age=∓36
|
|
266
|
-
const obj = parse(str, ageState); // { age: 36 }
|
|
267
|
-
// Or const obj = parse(str); // { age: 36 }
|
|
268
|
-
|
|
269
|
-
const currentParams = parse(window.location.search);
|
|
270
|
-
// OR
|
|
271
|
-
// const obj = parse(new URLSearchParams(window.location.search))
|
|
272
|
-
|
|
273
|
-
const updateSearch = () => {
|
|
274
|
-
const currentParams = new URLSearchParams(window.location.search);
|
|
275
|
-
const newState = { query: 'react hooks', page: 2 };
|
|
276
|
-
const updatedParamsString = stringify(newState, currentParams);
|
|
277
|
-
console.log(updatedParamsString);
|
|
278
|
-
// Output: existing params + query=react%20hooks&page=2
|
|
279
|
-
};
|
|
280
|
-
}
|
|
281
|
-
```
|
|
262
|
+
[Docs](packages/urlstate/useUrlEncode/README.md)
|
|
282
263
|
|
|
283
264
|
## `encodeState` and `decodeState` helpers
|
|
284
265
|
|
|
285
|
-
|
|
286
|
-
```ts
|
|
287
|
-
export const form = { name: '' };
|
|
288
|
-
...
|
|
289
|
-
encodeState({ name: 'test' }, form, 'someExistingParam=123');
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
`decodeState` let you decode queryString with optional defaults
|
|
293
|
-
```ts
|
|
294
|
-
export const form = { name: '' };
|
|
295
|
-
...
|
|
296
|
-
decodeState('name=Alex', form);
|
|
297
|
-
```
|
|
266
|
+
[Docs](packages/urlstate/encodeState/README.md)
|
|
298
267
|
|
|
299
268
|
## `encode` and `decode` helpers
|
|
300
269
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
```javascript
|
|
304
|
-
import { encode, decode } from 'state-in-url';
|
|
305
|
-
|
|
306
|
-
const state = { obj: [1, 2, 3], obj2: true }
|
|
307
|
-
|
|
308
|
-
// to params
|
|
309
|
-
const params = new URLSearchParams();
|
|
310
|
-
Object.entries(state).forEach(([key, value]) => {
|
|
311
|
-
params.set(key, encode(value));
|
|
312
|
-
});
|
|
313
|
-
const str = params.toString();
|
|
314
|
-
|
|
315
|
-
// from params
|
|
316
|
-
const obj = Object.fromEntries(
|
|
317
|
-
[...params.entries()].map(([key, value]) => [
|
|
318
|
-
key,
|
|
319
|
-
// decode(value, optionalFallback),
|
|
320
|
-
decode(value),
|
|
321
|
-
]),
|
|
322
|
-
)
|
|
323
|
-
```
|
|
270
|
+
[Docs](packages/urlstate/encoder/README.md)
|
|
324
271
|
|
|
325
272
|
## Best Practices
|
|
326
273
|
|
|
@@ -335,10 +282,13 @@ const obj = Object.fromEntries(
|
|
|
335
282
|
2. Vercel servers limit size of headers (query string and other stuff) to **14KB**, so keep your URL state under ~5000 words. https://vercel.com/docs/errors/URL_TOO_LONG
|
|
336
283
|
|
|
337
284
|
## Run locally
|
|
285
|
+
|
|
338
286
|
Clone this repo, run `npm install` and
|
|
287
|
+
|
|
339
288
|
```sh
|
|
340
289
|
npm run dev
|
|
341
290
|
```
|
|
291
|
+
|
|
342
292
|
Go to [localhost:3000](http://localhost:3000)
|
|
343
293
|
|
|
344
294
|
|
|
@@ -349,9 +299,11 @@ Go to [localhost:3000](http://localhost:3000)
|
|
|
349
299
|
## [Changelog](https://github.com/asmyshlyaev177/state-in-url/blob/main/CHANGELOG.md)
|
|
350
300
|
|
|
351
301
|
## License
|
|
302
|
+
|
|
352
303
|
This project is licensed under the [MIT license](https://github.com/asmyshlyaev177/state-in-url/blob/main/LICENSE).
|
|
353
304
|
|
|
354
305
|
## Inspiration
|
|
306
|
+
|
|
355
307
|
[Using URL to store state in Vue ](https://dev.to/jacobandrewsky/using-url-to-store-state-in-vue-275c)
|
|
356
308
|
|
|
357
309
|
[Storing state in the URL](https://antonz.org/storing-state/)
|
package/dist/index.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,"__esModule",{value:!0});var t=require("react"),e=require("next/navigation");function n(t){return t&&"object"==typeof t&&"default"in t?t:{default:t}}var r=n(t);const o=t=>{const e=null===t,n=Array.isArray(t),r=t instanceof Date;return(e?"null":r&&"date")||n&&"array"||!e&&!r&&!n&&"object"==typeof t&&"object"||typeof t},
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:!0});var t=require("react"),e=require("next/navigation");function n(t){return t&&"object"==typeof t&&"default"in t?t:{default:t}}var r=n(t);const c={string:"◖",boolean:"🗵",null:"∙null",undefined:"∙undefined",number:"∓",date:"⏲"},o=t=>{const e=null===t,n=Array.isArray(t),r=t instanceof Date;return(e?"null":r&&"date")||n&&"array"||!e&&!r&&!n&&"object"==typeof t&&"object"||typeof t},s=()=>"undefined"==typeof window,u=t=>new URLSearchParams("string"==typeof t?t:t?.toString?.()||""),a=(t,e)=>JSON.stringify(t)===JSON.stringify(e);function i(t){switch(o(t)){case"function":case"symbol":return"";case"date":return c.date+t.toISOString();case"string":return t.match(/^◖/)?encodeURIComponent(t):`${c.string}${encodeURIComponent(t)}`;case"number":return c.number+String(t);case"boolean":return c.boolean+String(t);case"object":case"array":return JSON.stringify(t,f).replaceAll('"',"'");case"null":return c.null;case"undefined":return c.undefined;default:return String(t)}}function l(t,e){return function(t,e){try{return JSON.parse(t,g)}catch{const n=d(t);return n!==p?n:e}}(t.replaceAll("'",'"'),e)}const f=(t,e)=>{const n=o(e);return t&&"object"!==n&&"array"!==n?i(e):e},d=t=>t===c.null?null:t!==c.undefined?t?.startsWith?.(c.number)?Number.parseFloat(t.replace(c.number,"")):t?.startsWith?.(c.boolean)?!!t.includes("true"):t?.startsWith?.(c.date)?new Date(t.slice(1)):t?.startsWith?.(c.string)?decodeURIComponent(t).replace(/^◖/,""):p:void 0,p=Symbol("isError"),g=(t,e)=>{const n="string"==typeof e,r=n&&d(e);return r===p?e:t&&n?r:e};const y=(t,e)=>{const n="string"==typeof e,r=n&&l(e?.replaceAll?.("'",'"'));return t&&n?r:e};function b(t,e){try{return JSON.parse(t,y)}catch{const n=d(t);return n!==p?n:e}}function S(t,e,n){const r=u(n);return Object.entries(t||{}).forEach((([t,n])=>{const c=e?.[t];JSON.stringify(n)!==JSON.stringify(c)&&r.set(t,i(n))})),r.toString()}function w(t,e){return{...e||{},...Object.fromEntries([...u(t).entries()].map((([t,n])=>{const r=e?.[t];return[t,l(n,r)??r]})))}}function h(t){const e=r.default.useCallback((function(e,n){return"object"===o(e)?S(e,t,n):""}),[t]);return{parse:r.default.useCallback((function(e){return w(e,t)}),[t]),stringify:e}}const m=new WeakMap,O=new WeakMap,j={get:function(t){return m.get(t)},set:function(t,e){m.set(t,e)}};exports.decode=l,exports.decodeState=w,exports.encode=i,exports.encodeState=S,exports.useUrlEncode=h,exports.useUrlState=function(t,n){const{state:c,getState:o,setState:u,stringify:i}=function(t){const e=r.default.useRef(t),{parse:n,stringify:c}=h(e.current),[o,u]=r.default.useState((()=>{const t=j.get(e.current),r=n(s()?"":window.location.search);return t||(j.set(e.current,r),r)})),i=r.default.useCallback((t=>{const n=j.get(e.current)||e.current;if("function"==typeof t){const r=t(n);if(a(n,r))return;j.set(e.current,r),(O.get(e.current)||[]).forEach((t=>{t()}))}else{if(a(n,t))return;j.set(e.current,t),(O.get(e.current)||[]).forEach((t=>{t()}))}}),[]);return r.default.useInsertionEffect((()=>{const t=O.get(e.current)||[],r=()=>{u(j.get(e.current)||e.current)};O.set(e.current,t.concat(r));const c=()=>{const t=n(window.location.search);i(t)},o="popstate";return window.addEventListener(o,c),()=>{const t=O.get(e.current)||[];O.set(e.current,t.filter((t=>t!==r))),window.removeEventListener(o,c)}}),[i]),{state:o,getState:r.default.useCallback((()=>j.get(e.current)||e.current),[]),setState:i,stringify:c}}(s()?function(t,e){return{...e,...b(JSON.stringify(t),e)}}(n,t):t),l=e.useRouter(),f=r.default.useCallback(((t,e)=>{const n=window.location.search,r=`${window.location.pathname}${n.length&&!n.includes("?")?"?":""}${n}`;let c,s;"function"==typeof t?(c=t(o()),s=i(c)):(c=t??o(),s=i(c)),u(c);const a=`${window.location.pathname}${s.length?"?":""}${s}`;if(r!==a){const{replace:t,...n}=e||{};l[t?"replace":"push"](a,{scroll:!1,...n})}}),[l,i,o]);return{updateState:u,updateUrl:f,state:c}};
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import t from"react";import{useRouter as e}from"next/navigation";const n=t=>{const e=null===t,n=Array.isArray(t),r=t instanceof Date;return(e?"null":r&&"date")||n&&"array"||!e&&!r&&!n&&"object"==typeof t&&"object"||typeof t},
|
|
1
|
+
import t from"react";import{useRouter as e}from"next/navigation";const n={string:"◖",boolean:"🗵",null:"∙null",undefined:"∙undefined",number:"∓",date:"⏲"},r=t=>{const e=null===t,n=Array.isArray(t),r=t instanceof Date;return(e?"null":r&&"date")||n&&"array"||!e&&!r&&!n&&"object"==typeof t&&"object"||typeof t},c=()=>"undefined"==typeof window,o=t=>new URLSearchParams("string"==typeof t?t:t?.toString?.()||""),s=(t,e)=>JSON.stringify(t)===JSON.stringify(e);function a(t){switch(r(t)){case"function":case"symbol":return"";case"date":return n.date+t.toISOString();case"string":return t.match(/^◖/)?encodeURIComponent(t):`${n.string}${encodeURIComponent(t)}`;case"number":return n.number+String(t);case"boolean":return n.boolean+String(t);case"object":case"array":return JSON.stringify(t,i).replaceAll('"',"'");case"null":return n.null;case"undefined":return n.undefined;default:return String(t)}}function u(t,e){return function(t,e){try{return JSON.parse(t,g)}catch{const n=l(t);return n!==f?n:e}}(t.replaceAll("'",'"'),e)}const i=(t,e)=>{const n=r(e);return t&&"object"!==n&&"array"!==n?a(e):e},l=t=>t===n.null?null:t!==n.undefined?t?.startsWith?.(n.number)?Number.parseFloat(t.replace(n.number,"")):t?.startsWith?.(n.boolean)?!!t.includes("true"):t?.startsWith?.(n.date)?new Date(t.slice(1)):t?.startsWith?.(n.string)?decodeURIComponent(t).replace(/^◖/,""):f:void 0,f=Symbol("isError"),g=(t,e)=>{const n="string"==typeof e,r=n&&l(e);return r===f?e:t&&n?r:e};const d=(t,e)=>{const n="string"==typeof e,r=n&&u(e?.replaceAll?.("'",'"'));return t&&n?r:e};function p(t,e){try{return JSON.parse(t,d)}catch{const n=l(t);return n!==f?n:e}}function y(t,e,n){const r=o(n);return Object.entries(t||{}).forEach((([t,n])=>{const c=e?.[t];JSON.stringify(n)!==JSON.stringify(c)&&r.set(t,a(n))})),r.toString()}function b(t,e){return{...e||{},...Object.fromEntries([...o(t).entries()].map((([t,n])=>{const r=e?.[t];return[t,u(n,r)??r]})))}}function S(e){const n=t.useCallback((function(t,n){return"object"===r(t)?y(t,e,n):""}),[e]);return{parse:t.useCallback((function(t){return b(t,e)}),[e]),stringify:n}}const m=new WeakMap,w=new WeakMap,h={get:function(t){return m.get(t)},set:function(t,e){m.set(t,e)}};function O(n,r){const{state:o,getState:a,setState:u,stringify:i}=function(e){const n=t.useRef(e),{parse:r,stringify:o}=S(n.current),[a,u]=t.useState((()=>{const t=h.get(n.current),e=r(c()?"":window.location.search);return t||(h.set(n.current,e),e)})),i=t.useCallback((t=>{const e=h.get(n.current)||n.current;if("function"==typeof t){const r=t(e);if(s(e,r))return;h.set(n.current,r),(w.get(n.current)||[]).forEach((t=>{t()}))}else{if(s(e,t))return;h.set(n.current,t),(w.get(n.current)||[]).forEach((t=>{t()}))}}),[]);return t.useInsertionEffect((()=>{const t=w.get(n.current)||[],e=()=>{u(h.get(n.current)||n.current)};w.set(n.current,t.concat(e));const c=()=>{const t=r(window.location.search);i(t)},o="popstate";return window.addEventListener(o,c),()=>{const t=w.get(n.current)||[];w.set(n.current,t.filter((t=>t!==e))),window.removeEventListener(o,c)}}),[i]),{state:a,getState:t.useCallback((()=>h.get(n.current)||n.current),[]),setState:i,stringify:o}}(c()?function(t,e){return{...e,...p(JSON.stringify(t),e)}}(r,n):n),l=e(),f=t.useCallback(((t,e)=>{const n=window.location.search,r=`${window.location.pathname}${n.length&&!n.includes("?")?"?":""}${n}`;let c,o;"function"==typeof t?(c=t(a()),o=i(c)):(c=t??a(),o=i(c)),u(c);const s=`${window.location.pathname}${o.length?"?":""}${o}`;if(r!==s){const{replace:t,...n}=e||{};l[t?"replace":"push"](s,{scroll:!1,...n})}}),[l,i,a]);return{updateState:u,updateUrl:f,state:o}}export{u as decode,b as decodeState,a as encode,y as encodeState,S as useUrlEncode,O as useUrlState};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './constants';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type JSONCompatible, type UnknownObj } from '
|
|
1
|
+
import { type JSONCompatible, type UnknownObj } from '../utils';
|
|
2
2
|
/**
|
|
3
3
|
* Encodes the state object into a URL query string.
|
|
4
4
|
*
|
|
@@ -12,7 +12,7 @@ import { type JSONCompatible, type UnknownObj } from './utils';
|
|
|
12
12
|
* encodeState({ name: 'test' }, form, 'someExistingParam=123');
|
|
13
13
|
* ```
|
|
14
14
|
*
|
|
15
|
-
* * Github {@link https://github.com/asmyshlyaev177/state-in-url}
|
|
15
|
+
* * Github {@link https://github.com/asmyshlyaev177/state-in-url/tree/main/packages/urlstate/encodeState#encodestate}
|
|
16
16
|
*/
|
|
17
17
|
export declare function encodeState<T extends JSONCompatible>(state: T, defaults?: T, paramsToKeep?: string | URLSearchParams): string;
|
|
18
18
|
/**
|
|
@@ -27,6 +27,6 @@ export declare function encodeState<T extends JSONCompatible>(state: T, defaults
|
|
|
27
27
|
* decodeState('key=value&name=Alex', form);
|
|
28
28
|
* ```
|
|
29
29
|
*
|
|
30
|
-
* * Github {@link https://github.com/asmyshlyaev177/state-in-url}
|
|
30
|
+
* * Github {@link https://github.com/asmyshlyaev177/state-in-url/tree/main/packages/urlstate/encodeState#decodestate}
|
|
31
31
|
*/
|
|
32
32
|
export declare function decodeState<T extends JSONCompatible>(uriString: string | URLSearchParams, defaults?: T): undefined extends T ? UnknownObj : T;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './encodeState';
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { type JSONCompatible } from '
|
|
1
|
+
import { type JSONCompatible } from '../utils';
|
|
2
2
|
/**
|
|
3
3
|
* Encode any JSON serializable value to URL friendly string
|
|
4
4
|
*
|
|
5
5
|
* @param {unknown} payload any value to serialize
|
|
6
6
|
* @returns {string} URLSearchParams compatible value string
|
|
7
7
|
*
|
|
8
|
-
* * Github {@link https://github.com/asmyshlyaev177/state-in-url}
|
|
8
|
+
* * Github {@link https://github.com/asmyshlyaev177/state-in-url/tree/main/packages/urlstate/encoder#encode}
|
|
9
9
|
*/
|
|
10
10
|
export declare function encode(payload: unknown): string;
|
|
11
11
|
/**
|
|
@@ -15,7 +15,7 @@ export declare function encode(payload: unknown): string;
|
|
|
15
15
|
* @param {any} fallback optional fallback value
|
|
16
16
|
* @returns {unknown} decoded object
|
|
17
17
|
*
|
|
18
|
-
* * Github {@link https://github.com/asmyshlyaev177/state-in-url}
|
|
18
|
+
* * Github {@link https://github.com/asmyshlyaev177/state-in-url/tree/main/packages/urlstate/encoder#decode}
|
|
19
19
|
*/
|
|
20
20
|
export declare function decode<T>(payload: string, fallback?: T): JSONCompatible | Primitive;
|
|
21
21
|
export declare const replacer: (key: string, value: unknown) => unknown;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './encoder';
|
package/dist/urlstate/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { decode, encode, parseSsrQs } from './encoder';
|
|
2
|
+
import { decodeState, encodeState } from './encodeState';
|
|
3
3
|
import { useUrlEncode } from './useUrlEncode';
|
|
4
4
|
import { useUrlState } from './useUrlState';
|
|
5
|
-
import {
|
|
6
|
-
export {
|
|
7
|
-
export type {
|
|
5
|
+
import { isSSR, type JSONCompatible, type Type, typeOf } from './utils';
|
|
6
|
+
export { decode, decodeState, encode, encodeState, isSSR, parseSsrQs, typeOf, useUrlEncode, useUrlState, };
|
|
7
|
+
export type { JSONCompatible, Type };
|
package/dist/urlstate/state.d.ts
CHANGED
|
@@ -2,6 +2,6 @@ import { type JSONCompatible } from './utils';
|
|
|
2
2
|
export declare function useState<T extends JSONCompatible>(defaultState: T): {
|
|
3
3
|
state: T;
|
|
4
4
|
getState: () => T;
|
|
5
|
-
setState: (value: T | ((currState:
|
|
5
|
+
setState: (value: T | ((currState: T) => T)) => void;
|
|
6
6
|
stringify: (state: T, paramsToKeep?: string | URLSearchParams | undefined) => string;
|
|
7
7
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './useUrlEncode';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type JSONCompatible } from '
|
|
1
|
+
import { type JSONCompatible } from '../utils';
|
|
2
2
|
/**
|
|
3
3
|
* A hook that returns stringify and parse functions for encoding and decoding state
|
|
4
4
|
* to and from URL search parameters.
|
|
@@ -17,9 +17,29 @@ import { type JSONCompatible } from './utils';
|
|
|
17
17
|
* parse('name=Tom');
|
|
18
18
|
* ```
|
|
19
19
|
*
|
|
20
|
-
* * Github {@link https://github.com/asmyshlyaev177/state-in-url}
|
|
20
|
+
* * Github {@link https://github.com/asmyshlyaev177/state-in-url/tree/main/packages/urlstate/useUrlEncode#api}
|
|
21
21
|
*/
|
|
22
22
|
export declare function useUrlEncode<T extends JSONCompatible>(stateShape: T): {
|
|
23
|
+
/**
|
|
24
|
+
Parse URL query string to state object
|
|
25
|
+
* * Example:
|
|
26
|
+
* ```ts
|
|
27
|
+
const state = parse('name=◖Tom');
|
|
28
|
+
console.log(state); // Output: { name: 'Tom' }
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* * Github {@link https://github.com/asmyshlyaev177/state-in-url/tree/main/packages/urlstate/useUrlEncode#parse}
|
|
32
|
+
*/
|
|
23
33
|
parse: (strOrSearchParams: string | URLSearchParams) => T;
|
|
34
|
+
/**
|
|
35
|
+
Converts a state object to a URL query string.
|
|
36
|
+
* * Example:
|
|
37
|
+
* ```ts
|
|
38
|
+
const queryString = stringify({ name: 'John' }, 'someExistingParamToKeep=123');
|
|
39
|
+
console.log(queryString); // Output: name=◖John&someExistingParamToKeep=123
|
|
40
|
+
* ```
|
|
41
|
+
*
|
|
42
|
+
* * Github {@link https://github.com/asmyshlyaev177/state-in-url/tree/main/packages/urlstate/useUrlEncode#stringify}
|
|
43
|
+
*/
|
|
24
44
|
stringify: (state: typeof stateShape, paramsToKeep?: string | URLSearchParams) => string;
|
|
25
45
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './useUrlState';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useRouter } from 'next/navigation';
|
|
2
|
-
import { DeepReadonly, type JSONCompatible } from '
|
|
2
|
+
import { type DeepReadonly, type JSONCompatible } from '../utils';
|
|
3
3
|
/**
|
|
4
4
|
* NextJS hook. Returns `state`, `updateState`, and `updateUrl` functions
|
|
5
5
|
*
|
|
@@ -16,11 +16,31 @@ import { DeepReadonly, type JSONCompatible } from './utils';
|
|
|
16
16
|
* updateUrl({ name: 'test' }, { replace: true, scroll: true });
|
|
17
17
|
* ```
|
|
18
18
|
*
|
|
19
|
-
* * Github {@link https://github.com/asmyshlyaev177/state-in-url}
|
|
19
|
+
* * Github {@link https://github.com/asmyshlyaev177/state-in-url/tree/main/packages/urlstate/useUrlState#api}
|
|
20
20
|
*/
|
|
21
21
|
export declare function useUrlState<T extends JSONCompatible>(defaultState: T, sp?: object): {
|
|
22
|
-
|
|
22
|
+
/**
|
|
23
|
+
* * Example:
|
|
24
|
+
* ```ts
|
|
25
|
+
* updateState({ name: 'test' });
|
|
26
|
+
* // or
|
|
27
|
+
* updateState(curr => ({ ...curr, name: 'test' }) );
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* * Github {@link https://github.com/asmyshlyaev177/state-in-url/tree/main/packages/urlstate/useUrlState#updatestate}
|
|
31
|
+
*/
|
|
23
32
|
updateState: (value: T | ((currState: T) => T)) => void;
|
|
33
|
+
/**
|
|
34
|
+
* * Example:
|
|
35
|
+
* ```ts
|
|
36
|
+
* updateUrl({ name: 'test' });
|
|
37
|
+
* // or
|
|
38
|
+
* updateUrl(curr => ({ ...curr, name: 'test' }), { replace: true, scroll: false } );
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* * Github {@link https://github.com/asmyshlyaev177/state-in-url/tree/main/packages/urlstate/useUrlState#updateurl}
|
|
42
|
+
*/
|
|
43
|
+
updateUrl: (value?: T | DeepReadonly<T> | ((currState: T) => T) | undefined, options?: Options) => void;
|
|
24
44
|
state: DeepReadonly<T>;
|
|
25
45
|
};
|
|
26
46
|
type Router = ReturnType<typeof useRouter>;
|
package/dist/urlstate/utils.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export type JSON = null | boolean | Date | number | string | {
|
|
|
15
15
|
export type JSONCompatible = {
|
|
16
16
|
[prop: string]: JSON | JSON[];
|
|
17
17
|
};
|
|
18
|
-
export type DeepReadonly<T> = T extends Map<infer K, infer V> ? ReadonlyMap<DeepReadonly<K>, DeepReadonly<V>> : T extends Set<infer S> ? ReadonlySet<DeepReadonly<S>> : T extends
|
|
18
|
+
export type DeepReadonly<T> = T extends Map<infer K, infer V> ? ReadonlyMap<DeepReadonly<K>, DeepReadonly<V>> : T extends Set<infer S> ? ReadonlySet<DeepReadonly<S>> : T extends object ? {
|
|
19
19
|
readonly [K in keyof T]: DeepReadonly<T[K]>;
|
|
20
20
|
} : T;
|
|
21
21
|
export declare const getParams: (strOrSearchParams?: string | URLSearchParams) => URLSearchParams;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "state-in-url",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
4
4
|
"description": "Library to store state in browser URL, includes hook for NextJS, hook for pure React, and low level helpers.",
|
|
5
5
|
"homepage": "https://github.com/asmyshlyaev177/state-in-url#readme",
|
|
6
6
|
"keywords": [
|
|
@@ -70,6 +70,7 @@
|
|
|
70
70
|
"test:int": "playwright test",
|
|
71
71
|
"test:int:watch": "npx chokidar-cli 'packages/**/*.ts' 'packages/**/*.tsx' -i '.next' -t 500 -c 'npx playwright test -j 2 --project=chromium'",
|
|
72
72
|
"test": "wireit",
|
|
73
|
+
"tsc": "wireit",
|
|
73
74
|
"dev": "wireit",
|
|
74
75
|
"dev:lib": "wireit",
|
|
75
76
|
"dev:nextjs": "wireit",
|
|
@@ -87,13 +88,20 @@
|
|
|
87
88
|
"wireit": {
|
|
88
89
|
"test": {
|
|
89
90
|
"dependencies": [
|
|
91
|
+
"tsc",
|
|
90
92
|
"build",
|
|
91
93
|
"test:unit",
|
|
92
94
|
"dev:nextjs"
|
|
93
95
|
],
|
|
94
96
|
"command": "npx playwright test --project=chromium -j 2"
|
|
95
97
|
},
|
|
98
|
+
"tsc": {
|
|
99
|
+
"command": "npx tsc --noEmit"
|
|
100
|
+
},
|
|
96
101
|
"test:unit": {
|
|
102
|
+
"dependencies": [
|
|
103
|
+
"tsc"
|
|
104
|
+
],
|
|
97
105
|
"command": "npx jest"
|
|
98
106
|
},
|
|
99
107
|
"dev": {
|
|
@@ -177,7 +185,7 @@
|
|
|
177
185
|
"cz": "^1.8.2",
|
|
178
186
|
"cz-conventional-changelog": "^3.3.0",
|
|
179
187
|
"eslint": "^8.57.0",
|
|
180
|
-
"eslint-plugin-maintainable": "^0.
|
|
188
|
+
"eslint-plugin-maintainable": "^0.4.4",
|
|
181
189
|
"husky": "^9.0.11",
|
|
182
190
|
"jest": "^29.7.0",
|
|
183
191
|
"jest-environment-jsdom": "^29.7.0",
|
|
File without changes
|