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
  ![演示-gif](https://github.com/asmyshlyaev177/state-in-url/assets/19854148/c9802601-4d42-4362-b3e4-37ff87c3b97f)
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
  [![npm](https://img.shields.io/npm/v/state-in-url.svg)](https://www.npmjs.com/package/state-in-url)
18
15
  ![Tests](https://github.com/asmyshlyaev177/state-in-url/actions/workflows/tests.yml/badge.svg?branch=main)
19
16
  [![Codacy Badge](https://app.codacy.com/project/badge/Grade/73be54068b7f41b0b74a252579ac09ec)](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
  ![Demo-gif](https://github.com/asmyshlyaev177/state-in-url/assets/19854148/c9802601-4d42-4362-b3e4-37ff87c3b97f)
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
- ## Why use state-in-url?
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, enhances developer experience with IDE suggestions, strong typing and JSDoc comments
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": "Node16"` or `"moduleResolution": "NodeNext"`
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
- // countState.ts
107
+ // userState.ts
115
108
  // State shape should be stored in a constant, don't pass an object directly
116
- export const countState: CountState = { count: 0 }
109
+ export const userState: UserState = { name: '', age: 0 }
117
110
 
118
- type CountState = { count: number }
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 { countState } from './countState';
120
+ import { userState } from './userState';
128
121
 
129
122
  function MyComponent() {
130
- // for use searchParams from server component
131
- // e.g. export default async function Home({ searchParams }: { searchParams: object }) {
132
- // const { state, updateState, updateUrl } = useUrlState(countState, searchParams);
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.count = 2 // <- error
128
+ // state.name = 'John' // <- error
137
129
 
138
130
  return (
139
131
  <div>
140
- <p>Count: {state.count}</p>
141
-
142
- <button onClick={() => updateUrl({ count: state.count + 1 }), { replace: true }}>
143
- Increment (Update URL)
144
- </button>
145
-
146
- // same api as React.useState
147
- <button onClick={() => updateState(currState => ({...currState, count: currState.count + 1 }) )}>
148
- Increment (Local Only)
149
- </button>
150
- <button onClick={() => updateUrl()}>
151
- Sync changes to url
152
- // Or don't sync it and just share state
153
- </button>
154
-
155
- <button onClick={() => updateUrl(state)}>
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
- interface UserSettings {
167
- theme: 'light' | 'dark';
168
- fontSize: number;
169
- notifications: boolean;
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 { userSettings } from './userSettings';
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
- return () => {
206
- clearTimeout(timer.current);
207
- };
208
- }, [state, updateUrl]);
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
- <h2>User Settings</h2>
213
- <p>Theme: {state.theme}</p>
214
- <p>Font Size: {state.fontSize}px</p>
215
- <button onClick={toggleTheme}>Toggle Theme</button>
216
- {/* Other UI elements to update other settings */}
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
- return (
227
- <div>
228
- <p>Notifications is {state.notifications ? 'On' : 'Off'}</p>
229
- </div>
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 sp={searchParams} />
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 = ({ sp }: { sp: object }) => {
267
- const { state, updateState, updateUrl } = useUrlState(form, sp);
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 ](https://dev.to/jacobandrewsky/using-url-to-store-state-in-vue-275c)
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
- * * Example:
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
- type Router = ReturnType<typeof useRouter>;
52
- type RouterOptions = NonNullable<Parameters<Router['push']>[1] | Parameters<Router['replace']>[1]>;
53
- interface Options extends RouterOptions {
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
- export {};
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 r from"react";import{parseSsrQs as a}from"../parseSsrQs.mjs";import{useUrlStateBase as s}from"../../useUrlStateBase/useUrlStateBase.mjs";function o(o,i){const m=t(),{state:n,updateState:u,updateUrl:c,getState:l}=s(o,m,i),p=r.useCallback(((t,e)=>{c(t,{scroll:!1,...e})}),[c]),f=e();return r.useEffect((()=>{const t=Object.keys(o),e=Object.fromEntries([...f.entries()].filter((([e])=>t.includes(e))));u(a(e,o))}),[f]),{updateState:u,updateUrl:p,state:n,getState:l}}export{o as useUrlState};
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,i,m){const{parse:w,stringify:u}=r(a),{state:l,getState:d,setState:f}=o(a,(()=>s()?e(function(t,e){const n=Object.keys(t),o=Object.fromEntries(Object.entries(e||{}).filter((([t])=>n.includes(t))));return o}(a,m),a):w(function(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()}(a))));n((()=>{const t=()=>{const t=w(window.location.search);f(t)};return window.addEventListener(c,t),()=>{window.removeEventListener(c,t)}}),[f]);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(d()):t?{...d(),...t}:d(),c=u(s,r);f(s);const m=`${window.location.pathname}${c.length?"?":""}${c}${window.location.hash}`;if(n!==m){const{replace:t,...n}=e||{};i[t?"replace":"push"](m,{...n})}}),[i,u,d]);return{updateState:f,updateUrl:h,state:l,getState:d}}const c="popstate";export{a as useUrlStateBase};
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.1.1",
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 -j 3 --project=chromium --retries=2",
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.2.0",
233
- "@testing-library/jest-dom": "^6.4.2",
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",