state-in-url 0.1.6 → 0.1.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
CHANGED
|
@@ -6,13 +6,28 @@
|
|
|
6
6
|
<img src="/assets/logo.svg?raw=true" alt="Library logo"/>
|
|
7
7
|
</p>
|
|
8
8
|
|
|
9
|
+
|
|
10
|
+
<div align="center">
|
|
11
|
+
|
|
12
|
+
[](https://www.linkedin.com/in/asmyshlyaev177/)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
[](https://www.npmjs.com/package/state-in-url)
|
|
16
|
+

|
|
17
|
+
[](https://github.com/asmyshlyaev177/state-in-url/)
|
|
18
|
+

|
|
19
|
+
</div>
|
|
20
|
+
|
|
9
21
|
<div align="center">
|
|
10
22
|
Seamlessly store complex React state object in URL query parameters without losing types. Works with Next.js/React.js applications.
|
|
11
23
|
|
|
12
24
|
<h4 align="center">Don't hesitate to open issue if you found a bug</h4>
|
|
13
25
|
|
|
14
26
|
|
|
15
|
-
|
|
27
|
+
https://github.com/asmyshlyaev177/state-in-url/assets/19854148/10456887-f149-4745-b3f1-e799e70b16cd
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
<a href="https://state-in-url-asmyshlyaev177.vercel.app/" target="_blank">DEMO</a>
|
|
16
31
|
|
|
17
32
|
|
|
18
33
|
<hr />
|
|
@@ -21,32 +36,33 @@ Add a ⭐️ to support the project!
|
|
|
21
36
|
|
|
22
37
|
<hr />
|
|
23
38
|
|
|
39
|
+
</div>
|
|
24
40
|
|
|
25
|
-
|
|
26
|
-
|
|
41
|
+
## Why use state-in-url?
|
|
27
42
|
|
|
28
|
-
|
|
29
|
-

|
|
30
|
-
[](https://github.com/asmyshlyaev177/state-in-url/)
|
|
31
|
-

|
|
43
|
+
`state-in-url` lets you use URI string for state management.
|
|
32
44
|
|
|
33
|
-
|
|
45
|
+
# Use cases
|
|
46
|
+
- 🔗 Shareable URLs with full application state
|
|
47
|
+
- 🔄 Easy state persistence across page reloads
|
|
48
|
+
- 🧠 Pass data between unrelated client components
|
|
49
|
+
- 🧮 Store unsaved user forms in URL
|
|
34
50
|
|
|
35
51
|
# Features
|
|
36
|
-
- **
|
|
37
|
-
- **
|
|
38
|
-
- **
|
|
39
|
-
- **
|
|
40
|
-
|
|
41
|
-
- **Efficient updates with minimal re-renders.**
|
|
42
|
-
- **Lightweight, no dependencies.**
|
|
52
|
+
- 🧩 **Simple**: Handles complex objects without extra effort
|
|
53
|
+
- 📘 **Typescript support and type Safety**: Preserves data types and structure, enhances developer experience with strong typing and JSDoc comments
|
|
54
|
+
- ⚛️ **Framework Flexibility**: Separate hooks for Next.js and React.js applications, and functions for pure JS
|
|
55
|
+
- 🪶 **Lightweight**: Zero dependencies for a smaller footprint
|
|
56
|
+
|
|
43
57
|
|
|
44
58
|
## Table of content
|
|
45
59
|
- [Installation](#installation)
|
|
46
60
|
- [`useUrlState` for Next.js](#useurlstate-hook-for-nextjs)
|
|
47
61
|
- [`useUrlEncode` for React.js](#useurlencode-hook-for-reactjs)
|
|
48
62
|
- [`encodeState` and `decodeState` for pure JS usage](#encodestate-and-decodestate-helpers)
|
|
63
|
+
- [auto sync state with url](#auto-sync-state)
|
|
49
64
|
- [Low-level `encode` and `decode` functions](#encode-and-decode-helpers)
|
|
65
|
+
- [Best practices](#best-practices)
|
|
50
66
|
- [Gothas](#gothas)
|
|
51
67
|
- [Contact & Support](#contact--support)
|
|
52
68
|
- [Changelog](#changelog)
|
|
@@ -63,12 +79,6 @@ yarn add state-in-url
|
|
|
63
79
|
pnpm add state-in-url
|
|
64
80
|
```
|
|
65
81
|
|
|
66
|
-
## Run locally
|
|
67
|
-
Clone this repo, run `npm install` and
|
|
68
|
-
```sh
|
|
69
|
-
npm run dev
|
|
70
|
-
```
|
|
71
|
-
Go to [localhost:3000](http://localhost:3000)
|
|
72
82
|
|
|
73
83
|
## useUrlState hook for Next.js
|
|
74
84
|
|
|
@@ -103,6 +113,7 @@ function MyComponent() {
|
|
|
103
113
|
<button onClick={() => updateState({ count: state.count + 1 })}>
|
|
104
114
|
Increment (Local Only)
|
|
105
115
|
</button>
|
|
116
|
+
|
|
106
117
|
<button onClick={() => updateUrl(state)}>
|
|
107
118
|
Sync changes to url
|
|
108
119
|
</button>
|
|
@@ -164,6 +175,23 @@ function Component() {
|
|
|
164
175
|
}
|
|
165
176
|
```
|
|
166
177
|
|
|
178
|
+
#### Auto sync state
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
const timer = React.useRef(0 as unknown as NodeJS.Timeout);
|
|
182
|
+
React.useEffect(() => {
|
|
183
|
+
clearTimeout(timer.current);
|
|
184
|
+
timer.current = setTimeout(() => {
|
|
185
|
+
// will compare state by content not by reference and fire update only for new values
|
|
186
|
+
updateUrl(state);
|
|
187
|
+
}, 500);
|
|
188
|
+
|
|
189
|
+
return () => {
|
|
190
|
+
clearTimeout(timer.current);
|
|
191
|
+
};
|
|
192
|
+
}, [state, updateUrl]);
|
|
193
|
+
```
|
|
194
|
+
|
|
167
195
|
#### With arbitrary state shape (not recommended)
|
|
168
196
|
|
|
169
197
|
```typescript
|
|
@@ -248,9 +276,25 @@ const obj = Object.fromEntries(
|
|
|
248
276
|
)
|
|
249
277
|
```
|
|
250
278
|
|
|
279
|
+
## Best Practices
|
|
280
|
+
|
|
281
|
+
- Define your state shape as a constant to ensure consistency
|
|
282
|
+
- Use TypeScript for enhanced type safety and autocomplete
|
|
283
|
+
- Avoid storing sensitive information in URL parameters
|
|
284
|
+
- Use `updateState` for frequent updates and `updateUrl` to sync changes to url
|
|
285
|
+
|
|
251
286
|
## Gothas
|
|
252
287
|
|
|
253
288
|
1. Can pass only serializable values, `Function`, `BigInt` or `Symbol` won't work, probably things like `ArrayBuffer` neither.
|
|
289
|
+
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
|
|
290
|
+
|
|
291
|
+
## Run locally
|
|
292
|
+
Clone this repo, run `npm install` and
|
|
293
|
+
```sh
|
|
294
|
+
npm run dev
|
|
295
|
+
```
|
|
296
|
+
Go to [localhost:3000](http://localhost:3000)
|
|
297
|
+
|
|
254
298
|
|
|
255
299
|
## Contact & Support
|
|
256
300
|
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import t from"react";import{useRouter as n,usePathname as e,useSearchParams as r}from"next/navigation";const o=t=>{const n=null===t,e=Array.isArray(t),r=t instanceof Date;return(n?"null":r&&"date")||e&&"array"||!n&&!r&&!e&&"object"==typeof t&&"object"||typeof t},s=t=>new URLSearchParams("string"==typeof t?t:t?.toString?.()||""),a={string:"◖",boolean:"🗵",null:"∙null",undefined:"∙undefined",number:"∓",date:"⏲"};function c(t){switch(o(t)){case"function":case"symbol":return"";case"date":return a.date+t.toISOString();case"string":return t.match(/^◖/)?encodeURIComponent(t):`${a.string}${encodeURIComponent(t)}`;case"number":return a.number+String(t);case"boolean":return a.boolean+String(t);case"object":case"array":return
|
|
1
|
+
import t from"react";import{useRouter as n,usePathname as e,useSearchParams as r}from"next/navigation";const o=t=>{const n=null===t,e=Array.isArray(t),r=t instanceof Date;return(n?"null":r&&"date")||e&&"array"||!n&&!r&&!e&&"object"==typeof t&&"object"||typeof t},s=t=>new URLSearchParams("string"==typeof t?t:t?.toString?.()||""),a={string:"◖",boolean:"🗵",null:"∙null",undefined:"∙undefined",number:"∓",date:"⏲"};function c(t){switch(o(t)){case"function":case"symbol":return"";case"date":return a.date+t.toISOString();case"string":return t.match(/^◖/)?encodeURIComponent(t):`${a.string}${encodeURIComponent(t)}`;case"number":return a.number+String(t);case"boolean":return a.boolean+String(t);case"object":case"array":return JSON.stringify(t,u).replaceAll('"',"'");case"null":return a.null;case"undefined":return a.undefined;default:return String(t)}}const u=(t,n)=>{const e=o(n);return t&&"object"!==e&&"array"!==e?c(n):n},i=t=>t===a.null?null:t!==a.undefined?t.startsWith(a.number)?Number.parseFloat(t.replace(a.number,"")):t.startsWith(a.boolean)?!!t.includes("true"):t.startsWith(a.date)?new Date(t.slice(1)):t.startsWith(a.string)?decodeURIComponent(t).replace(/^◖/,""):l:void 0,l=Symbol("isError"),f=(t,n)=>{const e="string"==typeof n,r=e&&i(n);return r===l?n:t&&e?r:n};function p(t,n){return function(t,n){try{return JSON.parse(t,f)}catch{const e=i(t);return e!==l?e:n}}(t.replaceAll("'",'"'),n)}function d(t,n,e){const r=s(e);return Object.entries(t||{}).forEach((([t,e])=>{const o=n?.[t];JSON.stringify(e)!==JSON.stringify(o)&&r.set(t,c(e))})),r.toString()}function b(t,n){return{...n||{},...Object.fromEntries([...s(t).entries()].map((([t,e])=>{const r=n?.[t];return[t,p(e,r)??r]})))}}function g(n){const e=t.useCallback((function(t,e){return"object"===o(t)?d(t,n,e):""}),[n]);return{parse:t.useCallback((function(t){return b(t,n)}),[n]),stringify:e}}function y(o){const s=n(),a=e(),{parse:c,stringify:u}=g(o),i=r(),[l,f]=t.useState((()=>c(i)));t.useEffect((()=>{f((t=>{const n=c(i);return JSON.stringify(t)===JSON.stringify(n)?t:n}))}),[c,i]);const p=t.useCallback((t=>{f("function"==typeof t?n=>t(n):t)}),[]);return{updateUrl:t.useCallback(((t,n)=>{const e=i.toString(),r=`${a}${e.length?"?":""}${e}`,o=u("function"==typeof t?t(l):t),c=`${a}${o.length?"?":""}${o}`;if(r!==c){const{replace:t,...e}=n||{};s[t?"replace":"push"](c,{scroll:!1,...e})}}),[a,s,u,i,l]),updateState:p,state:l}}export{p as decode,b as decodeState,c as encode,d as encodeState,g as useUrlEncode,y as useUrlState};
|
|
@@ -11,6 +11,8 @@ import { type JSONCompatible, type UnknownObj } from './utils';
|
|
|
11
11
|
* export const form = { name: '' };
|
|
12
12
|
* encodeState({ name: 'test' }, form, 'someExistingParam=123');
|
|
13
13
|
* ```
|
|
14
|
+
*
|
|
15
|
+
* * Github {@link https://github.com/asmyshlyaev177/state-in-url}
|
|
14
16
|
*/
|
|
15
17
|
export declare function encodeState<T>(state: never extends T ? object : JSONCompatible<T>, defaults?: JSONCompatible<T>, paramsToKeep?: string | URLSearchParams): string;
|
|
16
18
|
/**
|
|
@@ -24,5 +26,7 @@ export declare function encodeState<T>(state: never extends T ? object : JSONCom
|
|
|
24
26
|
* export const form = { name: '' };
|
|
25
27
|
* decodeState('key=value&name=Alex', form);
|
|
26
28
|
* ```
|
|
29
|
+
*
|
|
30
|
+
* * Github {@link https://github.com/asmyshlyaev177/state-in-url}
|
|
27
31
|
*/
|
|
28
32
|
export declare function decodeState<T>(uriString: string | URLSearchParams, defaults?: JSONCompatible<T>): undefined extends T ? UnknownObj : T;
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @param {unknown} payload any value to serialize
|
|
5
5
|
* @returns {string} URLSearchParams compatible value string
|
|
6
|
+
*
|
|
7
|
+
* * Github {@link https://github.com/asmyshlyaev177/state-in-url}
|
|
6
8
|
*/
|
|
7
9
|
export declare function encode(payload: unknown): string;
|
|
8
10
|
type Primitive = Exclude<ReturnType<typeof decodePrimitive>, typeof errorSym>;
|
|
@@ -14,6 +16,8 @@ export declare const errorSym: unique symbol;
|
|
|
14
16
|
* @param {string} jsonString - The JSON string to parse.
|
|
15
17
|
* @param {T} [fallbackValue] - The fallback value to use if parsing fails.
|
|
16
18
|
* @return {T | Primitive | undefined} - The parsed object or a primitive value, or undefined if parsing fails.
|
|
19
|
+
*
|
|
20
|
+
* * Github {@link https://github.com/asmyshlyaev177/state-in-url}
|
|
17
21
|
*/
|
|
18
22
|
export declare function parseJSON<T>(jsonString: string, fallbackValue?: T): T | Primitive | undefined;
|
|
19
23
|
/**
|
|
@@ -22,6 +26,8 @@ export declare function parseJSON<T>(jsonString: string, fallbackValue?: T): T |
|
|
|
22
26
|
* @param {string} payload URLSearchParams compatible value string
|
|
23
27
|
* @param {any} fallback optional fallback value
|
|
24
28
|
* @returns {unknown} decoded object
|
|
29
|
+
*
|
|
30
|
+
* * Github {@link https://github.com/asmyshlyaev177/state-in-url}
|
|
25
31
|
*/
|
|
26
32
|
export declare function decode<T>(payload: string, fallback?: T): Primitive | T;
|
|
27
33
|
export {};
|
|
@@ -16,6 +16,8 @@ import { type JSONCompatible, type DeepReadonly } from './utils';
|
|
|
16
16
|
* // by default it's uses router.push with scroll: false
|
|
17
17
|
* parse('name=Tom');
|
|
18
18
|
* ```
|
|
19
|
+
*
|
|
20
|
+
* * Github {@link https://github.com/asmyshlyaev177/state-in-url}
|
|
19
21
|
*/
|
|
20
22
|
export declare function useUrlEncode<T>(stateShape?: JSONCompatible<T>): {
|
|
21
23
|
parse: (strOrSearchParams: string | URLSearchParams) => undefined extends T ? Readonly<{
|
|
@@ -14,20 +14,8 @@ import { type JSONCompatible } from './utils';
|
|
|
14
14
|
* // by default it's uses router.push with scroll: false
|
|
15
15
|
* updateUrl({ name: 'test' }, { replace: true, scroll: true });
|
|
16
16
|
* ```
|
|
17
|
-
* * Auto update URL
|
|
18
|
-
* ```ts
|
|
19
|
-
* const timer = React.useRef(0 as unknown as NodeJS.Timeout);
|
|
20
|
-
* React.useEffect(() => {
|
|
21
|
-
* clearTimeout(timer.current);
|
|
22
|
-
* timer.current = setTimeout(() => {
|
|
23
|
-
* updateUrl(state);
|
|
24
|
-
* }, 500);
|
|
25
17
|
*
|
|
26
|
-
*
|
|
27
|
-
* clearTimeout(timer.current);
|
|
28
|
-
* };
|
|
29
|
-
* }, [state, updateUrl]);
|
|
30
|
-
* ```
|
|
18
|
+
* * Github {@link https://github.com/asmyshlyaev177/state-in-url}
|
|
31
19
|
*/
|
|
32
20
|
export declare function useUrlState<T>(defaultState?: JSONCompatible<T>): {
|
|
33
21
|
updateUrl: (value: (undefined extends T ? Readonly<{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "state-in-url",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.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": [
|