state-in-url 2.1.1 → 2.2.1
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.CN.md
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|

|
|
26
26
|
|
|
27
27
|
<a href="https://state-in-url-asmyshlyaev177.vercel.app/" target="_blank">演示</a> |
|
|
28
|
+
<a href="https://state-in-url.netlify.app/" target="_blank">DEMO2</a> |
|
|
28
29
|
<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>
|
|
29
30
|
|
|
30
31
|
<hr />
|
package/README.KO.md
CHANGED
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
<a href="https://state-in-url-asmyshlyaev177.vercel.app/" target="_blank">데모</a> |
|
|
32
|
+
<a href="https://state-in-url.netlify.app/" target="_blank">DEMO2</a> |
|
|
32
33
|
<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
34
|
|
|
34
35
|
|
package/README.md
CHANGED
|
@@ -2,18 +2,15 @@ English | [한국어](./README.KO.md) | [简体中文](./README.CN.md)
|
|
|
2
2
|
|
|
3
3
|
<h1 align="center">state-in-url</h1>
|
|
4
4
|
|
|
5
|
-
|
|
6
5
|
<div align="center">
|
|
7
6
|
<img src="/assets/logo.svg?raw=true" alt="Library logo"/>
|
|
8
7
|
|
|
9
8
|
<div>Easily share state between unrelated React components, with IDE autocomplete and TS validation. Without any hasssle or boilerplate. </div>
|
|
10
9
|
</div>
|
|
11
10
|
|
|
12
|
-
|
|
13
11
|
<div align="center">
|
|
14
12
|
<hr />
|
|
15
13
|
|
|
16
|
-
|
|
17
14
|
[](https://www.npmjs.com/package/state-in-url)
|
|
18
15
|

|
|
19
16
|
[](https://app.codacy.com/gh/asmyshlyaev177/state-in-url/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
|
|
@@ -23,15 +20,14 @@ English | [한국어](./README.KO.md) | [简体中文](./README.CN.md)
|
|
|
23
20
|
|
|
24
21
|
<div align="center">
|
|
25
22
|
|
|
26
|
-
<h4 align="center">Don't hesitate to open an issue if you found a bug</h4>
|
|
23
|
+
<h4 align="center">Don't hesitate to open an issue if you found a bug, or for requesting features</h4>
|
|
27
24
|
|
|
28
25
|

|
|
29
26
|
|
|
30
|
-
|
|
31
27
|
<a href="https://state-in-url-asmyshlyaev177.vercel.app/" target="_blank">DEMO</a> |
|
|
28
|
+
<a href="https://state-in-url.netlify.app/" target="_blank">DEMO2</a> |
|
|
32
29
|
<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
30
|
|
|
34
|
-
|
|
35
31
|
<hr />
|
|
36
32
|
|
|
37
33
|
Add a ⭐️ to support the project!
|
|
@@ -40,11 +36,10 @@ Add a ⭐️ to support the project!
|
|
|
40
36
|
|
|
41
37
|
</div>
|
|
42
38
|
|
|
43
|
-
|
|
39
|
+
## Why use state-in-url?
|
|
44
40
|
|
|
45
41
|
`state-in-url` Simple state management with optional URL sync. Share complex states between unrelated React components, TS-friendly, NextJS compatible.
|
|
46
42
|
|
|
47
|
-
|
|
48
43
|
# Use cases
|
|
49
44
|
|
|
50
45
|
- 🧮 Store unsaved user forms in URL
|
|
@@ -53,18 +48,15 @@ Add a ⭐️ to support the project!
|
|
|
53
48
|
- 🔗 Shareable URLs with full application state
|
|
54
49
|
- 🔄 Easy state persistence across page reloads
|
|
55
50
|
|
|
56
|
-
|
|
57
|
-
|
|
58
51
|
# Features
|
|
59
52
|
|
|
60
|
-
- 🧩 **Simple**: No providers, reducers, boilerplate or new concepts
|
|
61
|
-
- 📘 **Typescript support and type Safety**: Preserves data types and structure,
|
|
53
|
+
- 🧩 **Simple**: No providers, reducers, boilerplate or new concepts, API similar to React.useState
|
|
54
|
+
- 📘 **Typescript support and type Safety**: Preserves data types and structure, good developer experience with IDE suggestions, strong typing and JSDoc comments
|
|
62
55
|
- ⚛️ **Framework Flexibility**: Separate hooks for Next.js and React.js applications, and functions for pure JS
|
|
63
|
-
- ⚙ **Well tested**: Unit tests and Playwright tests
|
|
56
|
+
- ⚙ **Well tested**: Unit tests and Playwright tests, high quality and support
|
|
64
57
|
- ⚡ **Fast**: Minimal rerenders
|
|
65
58
|
- 🪶 **Lightweight**: Zero dependencies for a smaller footprint
|
|
66
59
|
|
|
67
|
-
|
|
68
60
|
## Table of content
|
|
69
61
|
|
|
70
62
|
- [Installation](#installation)
|
|
@@ -96,7 +88,8 @@ pnpm add state-in-url
|
|
|
96
88
|
|
|
97
89
|
### 2. Edit tsconfig.json
|
|
98
90
|
|
|
99
|
-
In `tsconfig.json` in `compilerOptions` set `"moduleResolution": "
|
|
91
|
+
In `tsconfig.json` in `compilerOptions` set `"moduleResolution": "Bundler"`, or`"moduleResolution": "Node16"`, or `"moduleResolution": "NodeNext"`.
|
|
92
|
+
Possibly need to set `"module": "ES2022"`, or `"module": "ESNext"`
|
|
100
93
|
|
|
101
94
|
## useUrlState hook for Next.js
|
|
102
95
|
|
|
@@ -111,11 +104,11 @@ In `tsconfig.json` in `compilerOptions` set `"moduleResolution": "Node16"` or `"
|
|
|
111
104
|
1. Define state shape
|
|
112
105
|
|
|
113
106
|
```typescript
|
|
114
|
-
//
|
|
107
|
+
// userState.ts
|
|
115
108
|
// State shape should be stored in a constant, don't pass an object directly
|
|
116
|
-
export const
|
|
109
|
+
export const userState: UserState = { name: '', age: 0 }
|
|
117
110
|
|
|
118
|
-
type
|
|
111
|
+
type UserState = { name: string, age: number }
|
|
119
112
|
```
|
|
120
113
|
|
|
121
114
|
2. Import it and use
|
|
@@ -124,37 +117,37 @@ In `tsconfig.json` in `compilerOptions` set `"moduleResolution": "Node16"` or `"
|
|
|
124
117
|
'use client'
|
|
125
118
|
import { useUrlState } from 'state-in-url/next';
|
|
126
119
|
|
|
127
|
-
import {
|
|
120
|
+
import { userState } from './userState';
|
|
128
121
|
|
|
129
122
|
function MyComponent() {
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
|
|
133
|
-
const { state, updateState, updateUrl } = useUrlState(countState);
|
|
123
|
+
// can pass `replace` arg, it's control will `updateUrl` will use `rounter.push` or `router.replace`, default replace=true
|
|
124
|
+
// const { state, updateState, updateUrl } = useUrlState({ defaultState: userState, searchParams, replace: false });
|
|
125
|
+
const { state, updateState, updateUrl } = useUrlState({ defaultState: userState });
|
|
134
126
|
|
|
135
127
|
// won't let you to accidently mutate state directly, requires TS
|
|
136
|
-
// state.
|
|
128
|
+
// state.name = 'John' // <- error
|
|
137
129
|
|
|
138
130
|
return (
|
|
139
131
|
<div>
|
|
140
|
-
<
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
<
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
<button onClick={() => updateUrl(
|
|
132
|
+
<input value={state.name}
|
|
133
|
+
onChange={(ev) => { updateState({ name: ev.target.value }) }}
|
|
134
|
+
onBlur={() => updateUrl()}
|
|
135
|
+
/>
|
|
136
|
+
<input value={state.age}
|
|
137
|
+
onChange={(ev) => { updateState({ age: +ev.target.value }) }}
|
|
138
|
+
onBlur={() => updateUrl()}
|
|
139
|
+
/>
|
|
140
|
+
|
|
141
|
+
// same api as React.useState
|
|
142
|
+
<input value={state.name}
|
|
143
|
+
onChange={(ev) => { updateState(curr => ({ ...curr, name: ev.target.value })) }}
|
|
144
|
+
onBlur={() => updateUrl()}
|
|
145
|
+
/>
|
|
146
|
+
|
|
147
|
+
<button onClick={() => updateUrl(userState)}>
|
|
156
148
|
Reset
|
|
157
149
|
</button>
|
|
150
|
+
|
|
158
151
|
</div>
|
|
159
152
|
)
|
|
160
153
|
}
|
|
@@ -163,74 +156,80 @@ function MyComponent() {
|
|
|
163
156
|
#### With complex state shape
|
|
164
157
|
|
|
165
158
|
```typescript
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
159
|
+
export const form: Form = {
|
|
160
|
+
name: '',
|
|
161
|
+
age: undefined,
|
|
162
|
+
'agree to terms': false,
|
|
163
|
+
tags: [],
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
type Form = {
|
|
167
|
+
name: string;
|
|
168
|
+
age?: number;
|
|
169
|
+
'agree to terms': boolean;
|
|
170
|
+
tags: { id: string; value: { text: string; time: Date } }[];
|
|
171
|
+
};
|
|
171
172
|
|
|
172
|
-
export const userSettings: UserSettings {
|
|
173
|
-
theme: 'light',
|
|
174
|
-
fontSize: 16,
|
|
175
|
-
notifications: true,
|
|
176
|
-
}
|
|
177
173
|
```
|
|
178
174
|
|
|
179
175
|
```typescript
|
|
180
176
|
'use client'
|
|
181
177
|
import { useUrlState } from 'state-in-url/next';
|
|
182
178
|
|
|
183
|
-
import {
|
|
184
|
-
|
|
185
|
-
function SettingsComponent() {
|
|
186
|
-
// `state` will infer from UserSettings type!
|
|
187
|
-
const { state, updateUrl } = useUrlState(userSettings);
|
|
188
|
-
|
|
189
|
-
const toggleTheme = () => {
|
|
190
|
-
updateUrl(current => ({
|
|
191
|
-
...current,
|
|
192
|
-
theme: current.theme === 'light' ? 'dark' : 'light',
|
|
193
|
-
}));
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
// sync state to url when idle
|
|
197
|
-
const timer = React.useRef(0 as unknown as NodeJS.Timeout);
|
|
198
|
-
React.useEffect(() => {
|
|
199
|
-
clearTimeout(timer.current);
|
|
200
|
-
timer.current = setTimeout(() => {
|
|
201
|
-
// will compare state by content not by reference and fire update only for new values
|
|
202
|
-
updateUrl(state);
|
|
203
|
-
}, 500);
|
|
179
|
+
import { form } from './form';
|
|
204
180
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
181
|
+
function TagsComponent() {
|
|
182
|
+
// `state` will infer from Form type!
|
|
183
|
+
const { state, updateUrl } = useUrlState({ defaultState: form });
|
|
184
|
+
|
|
185
|
+
const onChangeTags = React.useCallback(
|
|
186
|
+
(tag: (typeof tags)[number]) => {
|
|
187
|
+
updateUrl((curr) => ({
|
|
188
|
+
...curr,
|
|
189
|
+
tags: curr.tags.find((t) => t.id === tag.id)
|
|
190
|
+
? curr.tags.filter((t) => t.id !== tag.id)
|
|
191
|
+
: curr.tags.concat(tag),
|
|
192
|
+
}));
|
|
193
|
+
},
|
|
194
|
+
[updateUrl],
|
|
195
|
+
);
|
|
209
196
|
|
|
210
197
|
return (
|
|
211
198
|
<div>
|
|
212
|
-
<
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
199
|
+
<Field text="Tags">
|
|
200
|
+
<div className="flex flex-wrap gap-2">
|
|
201
|
+
{tags.map((tag) => (
|
|
202
|
+
<Tag
|
|
203
|
+
active={!!state.tags.find((t) => t.id === tag.id)}
|
|
204
|
+
text={tag.value.text}
|
|
205
|
+
onClick={() => onChangeTags(tag)}
|
|
206
|
+
key={tag.id}
|
|
207
|
+
/>
|
|
208
|
+
))}
|
|
209
|
+
</div>
|
|
210
|
+
</Field>
|
|
217
211
|
</div>
|
|
218
212
|
);
|
|
219
213
|
}
|
|
220
|
-
...
|
|
221
|
-
|
|
222
|
-
// Other component
|
|
223
|
-
function Component() {
|
|
224
|
-
const { state } = useUrlState(defaultSettings);
|
|
225
214
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
215
|
+
const tags = [
|
|
216
|
+
{
|
|
217
|
+
id: '1',
|
|
218
|
+
value: { text: 'React.js', time: new Date('2024-07-17T04:53:17.000Z') },
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
id: '2',
|
|
222
|
+
value: { text: 'Next.js', time: new Date('2024-07-18T04:53:17.000Z') },
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
id: '3',
|
|
226
|
+
value: { text: 'TailwindCSS', time: new Date('2024-07-19T04:53:17.000Z') },
|
|
227
|
+
},
|
|
228
|
+
];
|
|
232
229
|
```
|
|
233
230
|
|
|
231
|
+
[Demo page example code](https://github.com/asmyshlyaev177/state-in-url/blob/main/packages/example-nextjs/src/app/Form.tsx)
|
|
232
|
+
|
|
234
233
|
#### Auto sync state
|
|
235
234
|
|
|
236
235
|
```typescript
|
|
@@ -248,12 +247,18 @@ function Component() {
|
|
|
248
247
|
}, [state, updateUrl]);
|
|
249
248
|
```
|
|
250
249
|
|
|
250
|
+
Syncing state `onBlur` will be more aligned with real world usage.
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
<input onBlur={() => updateUrl()} .../>
|
|
254
|
+
```
|
|
255
|
+
|
|
251
256
|
#### With server side rendering
|
|
252
257
|
|
|
253
258
|
```typescript
|
|
254
259
|
export default async function Home({ searchParams }: { searchParams: object }) {
|
|
255
260
|
return (
|
|
256
|
-
<Form
|
|
261
|
+
<Form searchParams={searchParams} />
|
|
257
262
|
)
|
|
258
263
|
}
|
|
259
264
|
|
|
@@ -263,8 +268,8 @@ import React from 'react';
|
|
|
263
268
|
import { useUrlState } from 'state-in-url/next';
|
|
264
269
|
import { form } from './form';
|
|
265
270
|
|
|
266
|
-
const Form = ({
|
|
267
|
-
const { state, updateState, updateUrl } = useUrlState(form,
|
|
271
|
+
const Form = ({ searchParams }: { searchParams: object }) => {
|
|
272
|
+
const { state, updateState, updateUrl } = useUrlState({ defaultState: form, searchParams });
|
|
268
273
|
}
|
|
269
274
|
```
|
|
270
275
|
|
|
@@ -329,6 +334,7 @@ function SettingsComponent() {
|
|
|
329
334
|
```
|
|
330
335
|
|
|
331
336
|
## `useSharedState` hook for React.js
|
|
337
|
+
|
|
332
338
|
Hook to share state between any React components, tested with Next.js and Vite.
|
|
333
339
|
|
|
334
340
|
```typescript
|
|
@@ -368,7 +374,7 @@ function SettingsComponent() {
|
|
|
368
374
|
## Gothas
|
|
369
375
|
|
|
370
376
|
1. Can pass only serializable values, `Function`, `BigInt` or `Symbol` won't work, probably things like `ArrayBuffer` neither.
|
|
371
|
-
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
|
|
377
|
+
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>
|
|
372
378
|
3. Tested with `next.js` 14 with app router, no plans to support pages.
|
|
373
379
|
|
|
374
380
|
## Run locally
|
|
@@ -381,7 +387,6 @@ npm run dev
|
|
|
381
387
|
|
|
382
388
|
Go to [localhost:3000](http://localhost:3000)
|
|
383
389
|
|
|
384
|
-
|
|
385
390
|
## Contact & Support
|
|
386
391
|
|
|
387
392
|
- Create a [GitHub issue](https://github.com/asmyshlyaev177/state-in-url/issues) for bug reports, feature requests, or questions
|
|
@@ -394,7 +399,7 @@ This project is licensed under the [MIT license](https://github.com/asmyshlyaev1
|
|
|
394
399
|
|
|
395
400
|
## Inspiration
|
|
396
401
|
|
|
397
|
-
[Using URL to store state in Vue
|
|
402
|
+
[Using URL to store state in Vue](https://dev.to/jacobandrewsky/using-url-to-store-state-in-vue-275c)
|
|
398
403
|
|
|
399
404
|
[Storing state in the URL](https://antonz.org/storing-state/)
|
|
400
405
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { useRouter } from 'next/navigation';
|
|
2
1
|
import { type DeepReadonly, type JSONCompatible } from '../../utils';
|
|
3
2
|
/**
|
|
3
|
+
* @deprecated Pass arguments in a object `useUrlState({ defaultState: form, searchParams })`
|
|
4
|
+
*
|
|
4
5
|
* NextJS hook. Returns `state`, `updateState`, and `updateUrl` functions
|
|
5
6
|
*
|
|
6
7
|
* @param {JSONCompatible<T>} [defaultState] Fallback (default) values for state
|
|
@@ -23,34 +24,40 @@ import { type DeepReadonly, type JSONCompatible } from '../../utils';
|
|
|
23
24
|
* * Github {@link https://github.com/asmyshlyaev177/state-in-url/tree/main/packages/urlstate/next/useUrlState#api}
|
|
24
25
|
*/
|
|
25
26
|
export declare function useUrlState<T extends JSONCompatible>(defaultState: T, searchParams?: object): {
|
|
26
|
-
|
|
27
|
-
* * Example:
|
|
28
|
-
* ```ts
|
|
29
|
-
* updateState({ name: 'test' });
|
|
30
|
-
* // or
|
|
31
|
-
* updateState(curr => ({ ...curr, name: 'test' }) );
|
|
32
|
-
* ```
|
|
33
|
-
*
|
|
34
|
-
* * Github {@link https://github.com/asmyshlyaev177/state-in-url/tree/main/packages/urlstate/next/useUrlState#updatestate}
|
|
35
|
-
*/
|
|
27
|
+
state: DeepReadonly<T>;
|
|
36
28
|
updateState: (value: Partial<T> | Partial<DeepReadonly<T>> | ((currState: T) => T)) => void;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
* ```ts
|
|
40
|
-
* updateUrl({ name: 'test' });
|
|
41
|
-
* // or
|
|
42
|
-
* updateUrl(curr => ({ ...curr, name: 'test' }), { replace: true, scroll: false } );
|
|
43
|
-
* ```
|
|
44
|
-
*
|
|
45
|
-
* * Github {@link https://github.com/asmyshlyaev177/state-in-url/tree/main/packages/urlstate/next/useUrlState#updateurl}
|
|
46
|
-
*/
|
|
47
|
-
updateUrl: (value?: Partial<T> | Partial<DeepReadonly<T>> | ((currState: T) => T) | undefined, options?: Options) => void;
|
|
48
|
-
state: DeepReadonly<DeepReadonly<T>>;
|
|
49
|
-
getState: () => T;
|
|
29
|
+
updateUrl: (value?: Partial<T> | Partial<DeepReadonly<T>> | ((currState: T) => T)) => void;
|
|
30
|
+
getState: () => DeepReadonly<T>;
|
|
50
31
|
};
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
32
|
+
/**
|
|
33
|
+
* NextJS hook. Returns `state`, `updateState`, and `updateUrl` functions
|
|
34
|
+
*
|
|
35
|
+
* @param {JSONCompatible<T>} [defaultState] Fallback (default) values for state
|
|
36
|
+
* @param {?SearchParams<T>} [searchParams] searchParams from Next server component
|
|
37
|
+
*
|
|
38
|
+
* * Example:
|
|
39
|
+
* ```ts
|
|
40
|
+
* export const form = { name: '', age: 0 };
|
|
41
|
+
* const { state, updateState, updateUrl } = useUrlState({ defaultState: form });
|
|
42
|
+
* // for nextjs seerver components
|
|
43
|
+
* // const { state, updateState, updateUrl } = useUrlState({ defaultState: form, searchParams });
|
|
44
|
+
*
|
|
45
|
+
* updateState({ name: 'test' });
|
|
46
|
+
* // by default it's uses router.push with scroll: false
|
|
47
|
+
* updateUrl({ name: 'test' }, { replace: true, scroll: true });
|
|
48
|
+
* // similar to React.useState
|
|
49
|
+
* updateUrl(curr => ({ ...curr, name: 'test' }), { replace: true, scroll: true });
|
|
50
|
+
* ```
|
|
51
|
+
*
|
|
52
|
+
* * Github {@link https://github.com/asmyshlyaev177/state-in-url/tree/main/packages/urlstate/next/useUrlState#api}
|
|
53
|
+
*/
|
|
54
|
+
export declare function useUrlState<T extends JSONCompatible>({ defaultState, searchParams, }: {
|
|
55
|
+
defaultState: T;
|
|
56
|
+
searchParams?: object;
|
|
54
57
|
replace?: boolean;
|
|
55
|
-
}
|
|
56
|
-
|
|
58
|
+
}): {
|
|
59
|
+
state: DeepReadonly<T>;
|
|
60
|
+
updateState: (value: Partial<T> | Partial<DeepReadonly<T>> | ((currState: T) => T)) => void;
|
|
61
|
+
updateUrl: (value?: Partial<T> | Partial<DeepReadonly<T>> | ((currState: T) => T)) => void;
|
|
62
|
+
getState: () => DeepReadonly<T>;
|
|
63
|
+
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{useRouter as t,useSearchParams as e}from"next/navigation";import
|
|
1
|
+
import{useRouter as t,useSearchParams as e}from"next/navigation";import a from"react";import{parseSsrQs as r}from"../parseSsrQs.mjs";import{useUrlStateBase as s}from"../../useUrlStateBase/useUrlStateBase.mjs";function c(c,l){const{_defaultState:n,_searchParams:u,_replace:o}=function(t,e){const a="defaultState"in t?t.defaultState:t,r="defaultState"in t?t.searchParams:e,s="defaultState"in t&&(t.replace??!0);return{_defaultState:a,_searchParams:r,_replace:s}}(c,l),f=t(),{state:i,updateState:m,updateUrl:p,getState:S}=s(n,f,u),d=a.useCallback(((t,e)=>{const a={scroll:!1,replace:o,...e};p(t,a)}),[p,o]),_=e();return a.useEffect((()=>{const t=Object.keys(n),e=Object.fromEntries([..._.entries()].filter((([e])=>t.includes(e))));m(r(e,n))}),[_]),{updateState:m,updateUrl:d,state:i,getState:S}}export{c as useUrlState};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import t from"react";import{parseSsrQs as e}from"../next/parseSsrQs.mjs";import{useInsertionEffect as n}from"../useInsertionEffect.mjs";import{useSharedState as o}from"../useSharedState/useSharedState.mjs";import{useUrlEncode as r}from"../useUrlEncode/useUrlEncode.mjs";import{isSSR as s}from"../utils.mjs";function a(a,
|
|
1
|
+
import t from"react";import{parseSsrQs as e}from"../next/parseSsrQs.mjs";import{useInsertionEffect as n}from"../useInsertionEffect.mjs";import{useSharedState as o}from"../useSharedState/useSharedState.mjs";import{useUrlEncode as r}from"../useUrlEncode/useUrlEncode.mjs";import{isSSR as s}from"../utils.mjs";function a(a,m,l){const{parse:u,stringify:w}=r(a),{state:f,getState:p,setState:d}=o(a,(()=>s()?e(function(t,e){const n=Object.keys(t),o=Object.fromEntries(Object.entries(e||{}).map((([t,e])=>[t.replaceAll("+"," "),e])).filter((([t])=>n.includes(t))));return o}(a,l),a):u(c(a))));n((()=>{const t=()=>{const t=u(c(a));d(t)};return window.addEventListener(i,t),()=>{window.removeEventListener(i,t)}}),[d]);const h=t.useCallback(((t,e)=>{const n=`${window.location.pathname}${window.location.search}${window.location.hash}`,o="function"==typeof t,r=function(t){const e=Object.keys(t),n=window.location.search,o=new URLSearchParams(n),r=new URLSearchParams;return o.forEach(((t,n)=>!e.includes(n)&&r.set(n,t))),r}(a),s=o?t(p()):t?{...p(),...t}:p(),c=w(s,r);d(s);const i=`${window.location.pathname}${c.length?"?":""}${c}${window.location.hash}`;if(n!==i){const{replace:t,...n}=e||{};m[t?"replace":"push"](i,{...n})}}),[m,w,p]);return{updateState:d,updateUrl:h,state:f,getState:p}}function c(t){const e=new URLSearchParams(window.location.search),n=Object.keys(t),o=new URLSearchParams;return[...e.entries()].filter((([t])=>n.includes(t))).forEach((([t,e])=>o.set(t,e))),o.toString()}const i="popstate";export{a as useUrlStateBase};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "state-in-url",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.1",
|
|
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": [
|
|
@@ -120,7 +120,7 @@
|
|
|
120
120
|
]
|
|
121
121
|
},
|
|
122
122
|
"test:int": {
|
|
123
|
-
"command": "playwright test
|
|
123
|
+
"command": "playwright test --project=chromium --retries=2",
|
|
124
124
|
"dependencies": [
|
|
125
125
|
"example:react"
|
|
126
126
|
]
|
|
@@ -229,8 +229,8 @@
|
|
|
229
229
|
"@rollup/plugin-node-resolve": "^15.2.3",
|
|
230
230
|
"@rollup/plugin-terser": "^0.4.4",
|
|
231
231
|
"@rollup/plugin-typescript": "^11.1.6",
|
|
232
|
-
"@testing-library/dom": "^10.
|
|
233
|
-
"@testing-library/jest-dom": "^6.4.
|
|
232
|
+
"@testing-library/dom": "^10.4.0",
|
|
233
|
+
"@testing-library/jest-dom": "^6.4.8",
|
|
234
234
|
"@testing-library/react": "^16.0.0",
|
|
235
235
|
"@testing-library/react-hooks": "^8.0.1",
|
|
236
236
|
"@types/glob": "^8.1.0",
|