@multiplatform.one/router 5.0.26 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (249) hide show
  1. package/dist/cjs/hooks/index.cjs +31 -0
  2. package/dist/cjs/hooks/index.native.js +34 -0
  3. package/dist/cjs/hooks/index.native.js.map +1 -0
  4. package/dist/cjs/hooks/useUrlState.cjs +185 -0
  5. package/dist/cjs/hooks/useUrlState.native.js +203 -0
  6. package/dist/cjs/hooks/useUrlState.native.js.map +1 -0
  7. package/dist/cjs/hooks/useUrlState.spec.cjs +280 -0
  8. package/dist/cjs/hooks/useUrlState.spec.native.js +315 -0
  9. package/dist/cjs/hooks/useUrlState.spec.native.js.map +1 -0
  10. package/dist/cjs/index.cjs +9 -6
  11. package/dist/cjs/index.native.js +16 -13
  12. package/dist/cjs/index.native.js.map +1 -6
  13. package/dist/cjs/index.storybook.cjs +34 -0
  14. package/dist/cjs/index.storybook.native.js +37 -0
  15. package/dist/cjs/index.storybook.native.js.map +1 -0
  16. package/dist/cjs/router/EmptyRouter.cjs +78 -0
  17. package/dist/cjs/router/EmptyRouter.native.js +106 -0
  18. package/dist/cjs/router/EmptyRouter.native.js.map +1 -0
  19. package/dist/cjs/router/ReactRouter.cjs +107 -0
  20. package/dist/cjs/router/ReactRouter.native.js +135 -0
  21. package/dist/cjs/router/ReactRouter.native.js.map +1 -0
  22. package/dist/cjs/router/index.cjs +17 -67
  23. package/dist/cjs/router/index.native.js +22 -81
  24. package/dist/cjs/router/index.native.js.map +1 -6
  25. package/dist/cjs/router/index.storybook-expo.cjs +8 -6
  26. package/dist/cjs/router/index.storybook-expo.native.js +14 -11
  27. package/dist/cjs/router/index.storybook-expo.native.js.map +1 -6
  28. package/dist/cjs/router/index.storybook.cjs +8 -6
  29. package/dist/cjs/router/index.storybook.native.js +14 -11
  30. package/dist/cjs/router/index.storybook.native.js.map +1 -6
  31. package/dist/cjs/router/index.webext.cjs +8 -6
  32. package/dist/cjs/router/index.webext.native.js +14 -11
  33. package/dist/cjs/router/index.webext.native.js.map +1 -6
  34. package/dist/cjs/types.cjs +7 -5
  35. package/dist/cjs/types.native.js +11 -5
  36. package/dist/cjs/types.native.js.map +1 -6
  37. package/dist/esm/hooks/index.mjs +3 -0
  38. package/dist/esm/hooks/index.mjs.map +1 -0
  39. package/dist/esm/hooks/index.native.js +3 -0
  40. package/dist/esm/hooks/index.native.js.map +1 -0
  41. package/dist/esm/hooks/useUrlState.mjs +157 -0
  42. package/dist/esm/hooks/useUrlState.mjs.map +1 -0
  43. package/dist/esm/hooks/useUrlState.native.js +172 -0
  44. package/dist/esm/hooks/useUrlState.native.js.map +1 -0
  45. package/dist/esm/hooks/useUrlState.spec.mjs +257 -0
  46. package/dist/esm/hooks/useUrlState.spec.mjs.map +1 -0
  47. package/dist/esm/hooks/useUrlState.spec.native.js +289 -0
  48. package/dist/esm/hooks/useUrlState.spec.native.js.map +1 -0
  49. package/dist/esm/index.mjs +1 -0
  50. package/dist/esm/index.mjs.map +1 -1
  51. package/dist/esm/index.native.js +4 -3
  52. package/dist/esm/index.native.js.map +1 -6
  53. package/dist/esm/index.storybook.mjs +5 -0
  54. package/dist/esm/index.storybook.mjs.map +1 -0
  55. package/dist/esm/index.storybook.native.js +5 -0
  56. package/dist/esm/index.storybook.native.js.map +1 -0
  57. package/dist/esm/router/EmptyRouter.mjs +49 -0
  58. package/dist/esm/router/EmptyRouter.mjs.map +1 -0
  59. package/dist/esm/router/EmptyRouter.native.js +74 -0
  60. package/dist/esm/router/EmptyRouter.native.js.map +1 -0
  61. package/dist/esm/router/ReactRouter.mjs +79 -0
  62. package/dist/esm/router/ReactRouter.mjs.map +1 -0
  63. package/dist/esm/router/ReactRouter.native.js +104 -0
  64. package/dist/esm/router/ReactRouter.native.js.map +1 -0
  65. package/dist/esm/router/index.mjs +1 -53
  66. package/dist/esm/router/index.mjs.map +1 -1
  67. package/dist/esm/router/index.native.js +3 -71
  68. package/dist/esm/router/index.native.js.map +1 -6
  69. package/dist/esm/router/index.storybook-expo.mjs +1 -1
  70. package/dist/esm/router/index.storybook-expo.mjs.map +1 -1
  71. package/dist/esm/router/index.storybook-expo.native.js +2 -2
  72. package/dist/esm/router/index.storybook-expo.native.js.map +1 -6
  73. package/dist/esm/router/index.storybook.mjs +1 -1
  74. package/dist/esm/router/index.storybook.mjs.map +1 -1
  75. package/dist/esm/router/index.storybook.native.js +2 -2
  76. package/dist/esm/router/index.storybook.native.js.map +1 -6
  77. package/dist/esm/router/index.webext.mjs +1 -1
  78. package/dist/esm/router/index.webext.mjs.map +1 -1
  79. package/dist/esm/router/index.webext.native.js +2 -2
  80. package/dist/esm/router/index.webext.native.js.map +1 -6
  81. package/dist/esm/types.native.js +2 -1
  82. package/dist/esm/types.native.js.map +1 -6
  83. package/dist/jsx/hooks/index.mjs +3 -0
  84. package/dist/jsx/hooks/index.mjs.map +1 -0
  85. package/dist/jsx/hooks/index.native.js +34 -0
  86. package/dist/jsx/hooks/index.native.js.map +1 -0
  87. package/dist/jsx/hooks/useUrlState.mjs +157 -0
  88. package/dist/jsx/hooks/useUrlState.mjs.map +1 -0
  89. package/dist/jsx/hooks/useUrlState.native.js +203 -0
  90. package/dist/jsx/hooks/useUrlState.native.js.map +1 -0
  91. package/dist/jsx/hooks/useUrlState.spec.mjs +257 -0
  92. package/dist/jsx/hooks/useUrlState.spec.mjs.map +1 -0
  93. package/dist/jsx/hooks/useUrlState.spec.native.js +315 -0
  94. package/dist/jsx/hooks/useUrlState.spec.native.js.map +1 -0
  95. package/dist/jsx/index.js +3 -2
  96. package/dist/jsx/index.js.map +1 -6
  97. package/dist/jsx/index.mjs +1 -0
  98. package/dist/jsx/index.mjs.map +1 -1
  99. package/dist/jsx/index.native.js +25 -3
  100. package/dist/jsx/index.native.js.map +1 -6
  101. package/dist/jsx/index.storybook.mjs +5 -0
  102. package/dist/jsx/index.storybook.mjs.map +1 -0
  103. package/dist/jsx/index.storybook.native.js +37 -0
  104. package/dist/jsx/index.storybook.native.js.map +1 -0
  105. package/dist/jsx/router/EmptyRouter.mjs +49 -0
  106. package/dist/jsx/router/EmptyRouter.mjs.map +1 -0
  107. package/dist/jsx/router/EmptyRouter.native.js +106 -0
  108. package/dist/jsx/router/EmptyRouter.native.js.map +1 -0
  109. package/dist/jsx/router/ReactRouter.mjs +79 -0
  110. package/dist/jsx/router/ReactRouter.mjs.map +1 -0
  111. package/dist/jsx/router/ReactRouter.native.js +135 -0
  112. package/dist/jsx/router/ReactRouter.native.js.map +1 -0
  113. package/dist/jsx/router/index.mjs +1 -53
  114. package/dist/jsx/router/index.mjs.map +1 -1
  115. package/dist/jsx/router/index.native.js +31 -68
  116. package/dist/jsx/router/index.native.js.map +1 -6
  117. package/dist/jsx/router/index.storybook-expo.mjs +1 -1
  118. package/dist/jsx/router/index.storybook-expo.mjs.map +1 -1
  119. package/dist/jsx/router/index.storybook-expo.native.js +23 -2
  120. package/dist/jsx/router/index.storybook-expo.native.js.map +1 -6
  121. package/dist/jsx/router/index.storybook.mjs +1 -1
  122. package/dist/jsx/router/index.storybook.mjs.map +1 -1
  123. package/dist/jsx/router/index.storybook.native.js +23 -2
  124. package/dist/jsx/router/index.storybook.native.js.map +1 -6
  125. package/dist/jsx/router/index.webext.mjs +1 -1
  126. package/dist/jsx/router/index.webext.mjs.map +1 -1
  127. package/dist/jsx/router/index.webext.native.js +23 -2
  128. package/dist/jsx/router/index.webext.native.js.map +1 -6
  129. package/dist/jsx/types.native.js +21 -1
  130. package/dist/jsx/types.native.js.map +1 -6
  131. package/package.json +46 -36
  132. package/src/hooks/README.md +232 -0
  133. package/src/hooks/index.ts +8 -0
  134. package/src/hooks/useUrlState.spec.ts +219 -0
  135. package/src/hooks/useUrlState.ts +231 -0
  136. package/src/index.storybook.ts +3 -0
  137. package/src/index.ts +1 -21
  138. package/src/router/EmptyRouter.tsx +50 -0
  139. package/src/router/{reactRouter.tsx → ReactRouter.tsx} +27 -22
  140. package/src/router/index.storybook-expo.tsx +1 -22
  141. package/src/router/index.storybook.tsx +1 -22
  142. package/src/router/index.tsx +1 -97
  143. package/src/router/index.webext.tsx +1 -22
  144. package/src/types.tsx +3 -24
  145. package/dist/cjs/index.js +0 -16
  146. package/dist/cjs/index.js.map +0 -6
  147. package/dist/cjs/router/emptyRouter.cjs +0 -55
  148. package/dist/cjs/router/emptyRouter.js +0 -47
  149. package/dist/cjs/router/emptyRouter.js.map +0 -6
  150. package/dist/cjs/router/emptyRouter.native.js +0 -77
  151. package/dist/cjs/router/emptyRouter.native.js.map +0 -6
  152. package/dist/cjs/router/index.electron.cjs +0 -18
  153. package/dist/cjs/router/index.electron.js +0 -15
  154. package/dist/cjs/router/index.electron.js.map +0 -6
  155. package/dist/cjs/router/index.electron.native.js +0 -20
  156. package/dist/cjs/router/index.electron.native.js.map +0 -6
  157. package/dist/cjs/router/index.js +0 -63
  158. package/dist/cjs/router/index.js.map +0 -6
  159. package/dist/cjs/router/index.storybook-expo.js +0 -15
  160. package/dist/cjs/router/index.storybook-expo.js.map +0 -6
  161. package/dist/cjs/router/index.storybook.js +0 -15
  162. package/dist/cjs/router/index.storybook.js.map +0 -6
  163. package/dist/cjs/router/index.webext.js +0 -15
  164. package/dist/cjs/router/index.webext.js.map +0 -6
  165. package/dist/cjs/router/reactRouter.cjs +0 -74
  166. package/dist/cjs/router/reactRouter.js +0 -48
  167. package/dist/cjs/router/reactRouter.js.map +0 -6
  168. package/dist/cjs/router/reactRouter.native.js +0 -96
  169. package/dist/cjs/router/reactRouter.native.js.map +0 -6
  170. package/dist/cjs/types.js +0 -14
  171. package/dist/cjs/types.js.map +0 -6
  172. package/dist/esm/index.js +0 -3
  173. package/dist/esm/index.js.map +0 -6
  174. package/dist/esm/router/emptyRouter.js +0 -31
  175. package/dist/esm/router/emptyRouter.js.map +0 -6
  176. package/dist/esm/router/emptyRouter.mjs +0 -29
  177. package/dist/esm/router/emptyRouter.mjs.map +0 -1
  178. package/dist/esm/router/emptyRouter.native.js +0 -54
  179. package/dist/esm/router/emptyRouter.native.js.map +0 -6
  180. package/dist/esm/router/index.electron.js +0 -2
  181. package/dist/esm/router/index.electron.js.map +0 -6
  182. package/dist/esm/router/index.electron.mjs +0 -2
  183. package/dist/esm/router/index.electron.mjs.map +0 -1
  184. package/dist/esm/router/index.electron.native.js +0 -2
  185. package/dist/esm/router/index.electron.native.js.map +0 -6
  186. package/dist/esm/router/index.js +0 -52
  187. package/dist/esm/router/index.js.map +0 -6
  188. package/dist/esm/router/index.storybook-expo.js +0 -2
  189. package/dist/esm/router/index.storybook-expo.js.map +0 -6
  190. package/dist/esm/router/index.storybook.js +0 -2
  191. package/dist/esm/router/index.storybook.js.map +0 -6
  192. package/dist/esm/router/index.webext.js +0 -2
  193. package/dist/esm/router/index.webext.js.map +0 -6
  194. package/dist/esm/router/reactRouter.js +0 -38
  195. package/dist/esm/router/reactRouter.js.map +0 -6
  196. package/dist/esm/router/reactRouter.mjs +0 -48
  197. package/dist/esm/router/reactRouter.mjs.map +0 -1
  198. package/dist/esm/router/reactRouter.native.js +0 -74
  199. package/dist/esm/router/reactRouter.native.js.map +0 -6
  200. package/dist/esm/types.js +0 -1
  201. package/dist/esm/types.js.map +0 -6
  202. package/dist/jsx/router/emptyRouter.js +0 -31
  203. package/dist/jsx/router/emptyRouter.js.map +0 -6
  204. package/dist/jsx/router/emptyRouter.mjs +0 -29
  205. package/dist/jsx/router/emptyRouter.mjs.map +0 -1
  206. package/dist/jsx/router/emptyRouter.native.js +0 -54
  207. package/dist/jsx/router/emptyRouter.native.js.map +0 -6
  208. package/dist/jsx/router/index.electron.js +0 -2
  209. package/dist/jsx/router/index.electron.js.map +0 -6
  210. package/dist/jsx/router/index.electron.mjs +0 -2
  211. package/dist/jsx/router/index.electron.mjs.map +0 -1
  212. package/dist/jsx/router/index.electron.native.js +0 -2
  213. package/dist/jsx/router/index.electron.native.js.map +0 -6
  214. package/dist/jsx/router/index.js +0 -52
  215. package/dist/jsx/router/index.js.map +0 -6
  216. package/dist/jsx/router/index.storybook-expo.js +0 -2
  217. package/dist/jsx/router/index.storybook-expo.js.map +0 -6
  218. package/dist/jsx/router/index.storybook.js +0 -2
  219. package/dist/jsx/router/index.storybook.js.map +0 -6
  220. package/dist/jsx/router/index.webext.js +0 -2
  221. package/dist/jsx/router/index.webext.js.map +0 -6
  222. package/dist/jsx/router/reactRouter.js +0 -38
  223. package/dist/jsx/router/reactRouter.js.map +0 -6
  224. package/dist/jsx/router/reactRouter.mjs +0 -48
  225. package/dist/jsx/router/reactRouter.mjs.map +0 -1
  226. package/dist/jsx/router/reactRouter.native.js +0 -74
  227. package/dist/jsx/router/reactRouter.native.js.map +0 -6
  228. package/dist/jsx/types.js +0 -1
  229. package/dist/jsx/types.js.map +0 -6
  230. package/src/router/emptyRouter.tsx +0 -56
  231. package/src/router/index.electron.tsx +0 -22
  232. package/types/index.d.ts +0 -3
  233. package/types/index.d.ts.map +0 -1
  234. package/types/router/emptyRouter.d.ts +0 -26
  235. package/types/router/emptyRouter.d.ts.map +0 -1
  236. package/types/router/index.d.ts +0 -27
  237. package/types/router/index.d.ts.map +0 -1
  238. package/types/router/index.electron.d.ts +0 -22
  239. package/types/router/index.electron.d.ts.map +0 -1
  240. package/types/router/index.storybook-expo.d.ts +0 -22
  241. package/types/router/index.storybook-expo.d.ts.map +0 -1
  242. package/types/router/index.storybook.d.ts +0 -22
  243. package/types/router/index.storybook.d.ts.map +0 -1
  244. package/types/router/index.webext.d.ts +0 -22
  245. package/types/router/index.webext.d.ts.map +0 -1
  246. package/types/router/reactRouter.d.ts +0 -27
  247. package/types/router/reactRouter.d.ts.map +0 -1
  248. package/types/types.d.ts +0 -60
  249. package/types/types.d.ts.map +0 -1
package/package.json CHANGED
@@ -1,8 +1,7 @@
1
1
  {
2
2
  "name": "@multiplatform.one/router",
3
- "version": "5.0.26",
3
+ "version": "6.0.0",
4
4
  "description": "multiplatform.one router with support for react-router-dom and react-navigation",
5
- "sideEffects": false,
6
5
  "keywords": [
7
6
  "react",
8
7
  "react-native"
@@ -14,54 +13,65 @@
14
13
  },
15
14
  "license": "Apache-2.0",
16
15
  "author": "BitSpur <support@risserlabs.com> (https://risserlabs.com)",
17
- "main": "dist/cjs",
18
- "module": "dist/esm",
19
- "module:jsx": "dist/jsx",
20
- "source": "src/index.ts",
21
- "types": "types/index.d.ts",
22
- "exports": {
23
- ".": {
24
- "types": "./types/index.d.ts",
25
- "import": "./dist/esm/index.js",
26
- "require": "./dist/cjs/index.js"
27
- },
28
- "./package.json": "./package.json"
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://gitlab.com/bitspur/multiplatform.one/multiplatform.one"
29
19
  },
20
+ "source": "src/index.ts",
30
21
  "files": [
31
22
  "dist",
32
23
  "src",
33
24
  "types"
34
25
  ],
35
- "repository": {
36
- "type": "git",
37
- "url": "https://gitlab.com/bitspur/multiplatform.one/multiplatform.one"
26
+ "sideEffects": false,
27
+ "main": "src/index.ts",
28
+ "module": "src/index.ts",
29
+ "types": "types/index.d.ts",
30
+ "exports": {
31
+ "./package.json": "./package.json",
32
+ ".": {
33
+ "react-native": {
34
+ "import": "./dist/esm/index.native.js",
35
+ "require": "./dist/cjs/index.native.js"
36
+ },
37
+ "source": "./src/index.ts",
38
+ "types": "./types/index.d.ts",
39
+ "import": "./dist/esm/index.js",
40
+ "require": "./dist/cjs/index.js",
41
+ "default": "./dist/cjs/index.native.js"
42
+ }
38
43
  },
39
- "engines": {
40
- "node": ">=16.0.0"
44
+ "publishConfig": {
45
+ "access": "public"
41
46
  },
42
47
  "dependencies": {
43
- "multiplatform.one": "5.0.26"
48
+ "@multiplatform.one/platform": "6.0.0"
44
49
  },
45
50
  "devDependencies": {
46
- "@react-navigation/native": "^7.0.14",
47
- "@tamagui/build": "^1.122.6",
48
- "@testing-library/react": "^14.1.2",
49
- "@vitejs/plugin-react": "^4.2.1",
50
- "jsdom": "^23.0.1",
51
- "one": "^1.1.401",
52
- "react": "~18.2.0",
53
- "react-dom": "~18.2.0",
54
- "react-native": "0.74.5",
55
- "react-router-dom": "^6.28.2",
56
- "typescript": "~5.3.3",
57
- "vite": "^6.0.3",
58
- "vitest": "^1.4.0"
51
+ "@react-navigation/native": "^7.2.2",
52
+ "@tamagui/build": "2.0.0-rc.41",
53
+ "@testing-library/react": "^16.3.2",
54
+ "@vitejs/plugin-react": "^6.0.1",
55
+ "one": "^1.16.5",
56
+ "react": "19.2.5",
57
+ "react-dom": "19.2.5",
58
+ "react-native": "0.83.2",
59
+ "react-router-dom": "^7.14.2",
60
+ "typescript": "~5.9.3",
61
+ "vite": "^8.0.10",
62
+ "vitest": "^4.1.5",
63
+ "@multiplatform.one/test-utils": "6.0.0",
64
+ "@multiplatform.one/config": "6.0.0"
59
65
  },
60
66
  "peerDependencies": {
61
- "one": "^1.1.345",
62
- "react": "~18.2.0",
63
- "react-router-dom": "^6.28.0"
67
+ "one": "^1.4.10",
68
+ "react": "^19.1.0",
69
+ "react-router-dom": "^7.0.0"
70
+ },
71
+ "engines": {
72
+ "node": ">=16.0.0"
64
73
  },
74
+ "module:jsx": "dist/jsx",
65
75
  "scripts": {
66
76
  "build": "rm -rf dist types 2>/dev/null && tamagui-build --ts-project ./tsconfig.build.json",
67
77
  "test": "vitest run --coverage --coverage.provider=v8 --coverage.reporter=text --coverage.reporter=lcov --coverage.reporter=html",
@@ -0,0 +1,232 @@
1
+ # URL State Management Hooks
2
+
3
+ Cross-platform hooks for managing URL state in forms and other components. These hooks work seamlessly across web (React Router) and native (React Navigation) platforms.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @multiplatform.one/router
9
+ ```
10
+
11
+ ## Hooks
12
+
13
+ ### `useSearchParams`
14
+
15
+ Basic hook for reading and writing URL search parameters.
16
+
17
+ ```typescript
18
+ import { useSearchParams } from "@multiplatform.one/router";
19
+
20
+ function SearchForm() {
21
+ const [searchParams, setSearchParams] = useSearchParams();
22
+
23
+ const handleSearch = (query: string) => {
24
+ setSearchParams({ q: query }, { debounceMs: 300 });
25
+ };
26
+
27
+ return (
28
+ <input
29
+ value={searchParams.q || ""}
30
+ onChange={(e) => handleSearch(e.target.value)}
31
+ />
32
+ );
33
+ }
34
+ ```
35
+
36
+ ### `useUrlState`
37
+
38
+ Advanced hook for syncing complex form state with URL parameters, with automatic serialization/deserialization.
39
+
40
+ ```typescript
41
+ import { useUrlState } from "@multiplatform.one/router";
42
+
43
+ interface FormState {
44
+ name: string;
45
+ age: number;
46
+ tags: string[];
47
+ settings: {
48
+ theme: "light" | "dark";
49
+ notifications: boolean;
50
+ };
51
+ }
52
+
53
+ function MyForm() {
54
+ const [formState, setFormState] = useUrlState<FormState>({
55
+ name: "",
56
+ age: 0,
57
+ tags: [],
58
+ settings: {
59
+ theme: "light",
60
+ notifications: true,
61
+ },
62
+ }, {
63
+ debounceMs: 500, // Debounce URL updates by 500ms
64
+ });
65
+
66
+ return (
67
+ <form>
68
+ <input
69
+ value={formState.name}
70
+ onChange={(e) => setFormState({ name: e.target.value })}
71
+ />
72
+ {/* Form fields... */}
73
+ </form>
74
+ );
75
+ }
76
+ ```
77
+
78
+ ### `useTypedSearchParams`
79
+
80
+ Type-safe parsing of URL parameters with schema validation.
81
+
82
+ ```typescript
83
+ import { useTypedSearchParams } from "@multiplatform.one/router";
84
+
85
+ function DataTable() {
86
+ const params = useTypedSearchParams({
87
+ page: {
88
+ default: 1,
89
+ parse: (value) => value ? parseInt(String(value), 10) : 1,
90
+ },
91
+ limit: {
92
+ default: 20,
93
+ parse: (value) => value ? parseInt(String(value), 10) : 20,
94
+ },
95
+ sortBy: {
96
+ default: "created",
97
+ },
98
+ sortOrder: {
99
+ default: "desc" as "asc" | "desc",
100
+ parse: (value) => value === "asc" ? "asc" : "desc",
101
+ },
102
+ });
103
+
104
+ // params is fully typed: { page: number; limit: number; sortBy: string; sortOrder: "asc" | "desc" }
105
+
106
+ return (
107
+ <div>
108
+ Page {params.page} of results
109
+ </div>
110
+ );
111
+ }
112
+ ```
113
+
114
+ ### `useShareableUrl`
115
+
116
+ Generate a shareable URL with current state.
117
+
118
+ ```typescript
119
+ import { useShareableUrl } from "@multiplatform.one/router";
120
+
121
+ function ShareButton() {
122
+ const shareableUrl = useShareableUrl();
123
+
124
+ const handleShare = () => {
125
+ navigator.clipboard.writeText(shareableUrl);
126
+ };
127
+
128
+ return (
129
+ <button onClick={handleShare}>
130
+ Copy Link: {shareableUrl}
131
+ </button>
132
+ );
133
+ }
134
+ ```
135
+
136
+ ## Integration with TanStack Form
137
+
138
+ Example of integrating URL state with TanStack Form:
139
+
140
+ ```typescript
141
+ import { useForm } from "@tanstack/react-form";
142
+ import { useUrlState } from "@multiplatform.one/router";
143
+
144
+ function UrlSyncedForm() {
145
+ const [urlState, setUrlState] = useUrlState({
146
+ name: "",
147
+ email: "",
148
+ subscribe: false,
149
+ });
150
+
151
+ const form = useForm({
152
+ defaultValues: urlState,
153
+ onSubmit: async ({ value }) => {
154
+ // Submit logic
155
+ console.log("Submitting:", value);
156
+ },
157
+ });
158
+
159
+ // Sync form changes to URL
160
+ useEffect(() => {
161
+ const subscription = form.subscribe((state) => {
162
+ if (state.values) {
163
+ setUrlState(state.values);
164
+ }
165
+ });
166
+ return () => subscription.unsubscribe();
167
+ }, [form, setUrlState]);
168
+
169
+ return (
170
+ <form.Provider>
171
+ <form onSubmit={(e) => {
172
+ e.preventDefault();
173
+ form.handleSubmit();
174
+ }}>
175
+ <form.Field name="name">
176
+ {(field) => (
177
+ <input
178
+ value={field.state.value}
179
+ onChange={(e) => field.handleChange(e.target.value)}
180
+ onBlur={field.handleBlur}
181
+ />
182
+ )}
183
+ </form.Field>
184
+ {/* More fields... */}
185
+ </form>
186
+ </form.Provider>
187
+ );
188
+ }
189
+ ```
190
+
191
+ ## Features
192
+
193
+ - ✅ **Cross-platform**: Works on web, iOS, and Android
194
+ - ✅ **Type-safe**: Full TypeScript support
195
+ - ✅ **Debouncing**: Built-in debounce support for performance
196
+ - ✅ **SSR-friendly**: Works with server-side rendering
197
+ - ✅ **Flexible serialization**: Customize how state is stored in URLs
198
+ - ✅ **Array & object support**: Handle complex data structures
199
+ - ✅ **Default values**: Automatic fallback to defaults
200
+
201
+ ## Best Practices
202
+
203
+ 1. **Debounce text inputs** - Use 300-500ms debounce for text fields
204
+ 2. **Update on blur** - For better UX, consider updating URL on field blur
205
+ 3. **Keep URLs readable** - Use meaningful parameter names
206
+ 4. **Limit URL length** - Store only essential state in URLs
207
+ 5. **Handle hydration** - Use `defaultValue` for SSR compatibility
208
+
209
+ ## SSR Considerations
210
+
211
+ These hooks are SSR-safe. During server-side rendering:
212
+
213
+ - URL parameters are read from the request
214
+ - Forms are pre-populated with URL values
215
+ - Hydration preserves user input
216
+
217
+ ```typescript
218
+ // SSR-safe form with URL state
219
+ function SSRForm() {
220
+ const [state, setState] = useUrlState(
221
+ { search: "" },
222
+ { debounceMs: 500 }
223
+ );
224
+
225
+ return (
226
+ <input
227
+ defaultValue={state.search} // Use defaultValue for SSR
228
+ onChange={(e) => setState({ search: e.target.value })}
229
+ />
230
+ );
231
+ }
232
+ ```
@@ -0,0 +1,8 @@
1
+ export {
2
+ useSearchParams,
3
+ useUrlState,
4
+ useShareableUrl,
5
+ useTypedSearchParams,
6
+ type SearchParams,
7
+ type SetSearchParamsOptions,
8
+ } from "./useUrlState";
@@ -0,0 +1,219 @@
1
+ import { act, renderHook, waitFor } from "@testing-library/react";
2
+ import { beforeEach, describe, expect, it, vi } from "vitest";
3
+ import * as router from "../router";
4
+ import { useSearchParams, useTypedSearchParams, useUrlState } from "./useUrlState";
5
+
6
+ // Mock the router module
7
+ vi.mock("../router", () => {
8
+ const mockSetParams = vi.fn();
9
+ return {
10
+ useParams: vi.fn(() => ({})),
11
+ useRouter: vi.fn(() => ({
12
+ setParams: mockSetParams,
13
+ })),
14
+ };
15
+ });
16
+
17
+ // Get the mocked functions
18
+ const mockUseParams = router.useParams as ReturnType<typeof vi.fn>;
19
+ const mockUseRouter = router.useRouter as ReturnType<typeof vi.fn>;
20
+
21
+ describe("useUrlState", () => {
22
+ beforeEach(() => {
23
+ vi.clearAllMocks();
24
+ });
25
+
26
+ describe("useSearchParams", () => {
27
+ it("should return current search params", () => {
28
+ mockUseParams.mockReturnValue({ foo: "bar", baz: "qux" });
29
+
30
+ const { result } = renderHook(() => useSearchParams());
31
+ const [params] = result.current;
32
+
33
+ expect(params).toEqual({ foo: "bar", baz: "qux" });
34
+ });
35
+
36
+ it("should update search params", () => {
37
+ const setParamsMock = vi.fn();
38
+ mockUseRouter.mockReturnValue({ setParams: setParamsMock });
39
+
40
+ const { result } = renderHook(() => useSearchParams());
41
+ const [, setParams] = result.current;
42
+
43
+ act(() => {
44
+ setParams({ foo: "bar" });
45
+ });
46
+
47
+ expect(setParamsMock).toHaveBeenCalledWith({ foo: "bar" });
48
+ });
49
+
50
+ it("should support function updates", () => {
51
+ const setParamsMock = vi.fn();
52
+ mockUseParams.mockReturnValue({ existing: "value" });
53
+ mockUseRouter.mockReturnValue({ setParams: setParamsMock });
54
+
55
+ const { result } = renderHook(() => useSearchParams());
56
+ const [, setParams] = result.current;
57
+
58
+ act(() => {
59
+ setParams((prev) => ({ ...prev, new: "param" }));
60
+ });
61
+
62
+ expect(setParamsMock).toHaveBeenCalledWith({
63
+ existing: "value",
64
+ new: "param",
65
+ });
66
+ });
67
+
68
+ it("should debounce updates when specified", async () => {
69
+ const setParamsMock = vi.fn();
70
+ mockUseRouter.mockReturnValue({ setParams: setParamsMock });
71
+
72
+ const { result } = renderHook(() => useSearchParams());
73
+ const [, setParams] = result.current;
74
+
75
+ act(() => {
76
+ setParams({ foo: "bar" }, { debounceMs: 100 });
77
+ });
78
+
79
+ expect(setParamsMock).not.toHaveBeenCalled();
80
+
81
+ await waitFor(
82
+ () => {
83
+ expect(setParamsMock).toHaveBeenCalledWith({ foo: "bar" });
84
+ },
85
+ { timeout: 200 },
86
+ );
87
+ });
88
+ });
89
+
90
+ describe("useUrlState", () => {
91
+ it("should initialize with default values", () => {
92
+ mockUseParams.mockReturnValue({});
93
+
94
+ const defaultValues = { name: "", age: 0, active: false };
95
+ const { result } = renderHook(() => useUrlState(defaultValues));
96
+ const [state] = result.current;
97
+
98
+ expect(state).toEqual(defaultValues);
99
+ });
100
+
101
+ it("should deserialize URL params", () => {
102
+ mockUseParams.mockReturnValue({
103
+ name: "John",
104
+ age: "25",
105
+ active: "true",
106
+ });
107
+
108
+ const defaultValues = { name: "", age: 0, active: false };
109
+ const { result } = renderHook(() => useUrlState(defaultValues));
110
+ const [state] = result.current;
111
+
112
+ expect(state).toEqual({
113
+ name: "John",
114
+ age: 25,
115
+ active: true,
116
+ });
117
+ });
118
+
119
+ it("should serialize state to URL params", () => {
120
+ const setParamsMock = vi.fn();
121
+ mockUseParams.mockReturnValue({});
122
+ mockUseRouter.mockReturnValue({ setParams: setParamsMock });
123
+
124
+ const defaultValues = { name: "", age: 0, active: false };
125
+ const { result } = renderHook(() => useUrlState(defaultValues, { debounceMs: 0 }));
126
+ const [, setState] = result.current;
127
+
128
+ act(() => {
129
+ setState({ name: "John", age: 25, active: true });
130
+ });
131
+
132
+ expect(setParamsMock).toHaveBeenCalledWith({
133
+ name: "John",
134
+ age: "25",
135
+ active: "true",
136
+ });
137
+ });
138
+
139
+ it("should handle arrays", () => {
140
+ mockUseParams.mockReturnValue({
141
+ tags: ["react", "typescript"],
142
+ });
143
+
144
+ const defaultValues = { tags: [] as string[] };
145
+ const { result } = renderHook(() => useUrlState(defaultValues));
146
+ const [state] = result.current;
147
+
148
+ expect(state).toEqual({
149
+ tags: ["react", "typescript"],
150
+ });
151
+ });
152
+
153
+ it("should handle objects with JSON serialization", () => {
154
+ mockUseParams.mockReturnValue({
155
+ config: '{"theme":"dark","lang":"en"}',
156
+ });
157
+
158
+ const defaultValues = { config: { theme: "light", lang: "en" } };
159
+ const { result } = renderHook(() => useUrlState(defaultValues));
160
+ const [state] = result.current;
161
+
162
+ expect(state).toEqual({
163
+ config: { theme: "dark", lang: "en" },
164
+ });
165
+ });
166
+ });
167
+
168
+ describe("useTypedSearchParams", () => {
169
+ it("should parse params with schema", () => {
170
+ mockUseParams.mockReturnValue({
171
+ page: "2",
172
+ limit: "50",
173
+ search: "test",
174
+ });
175
+
176
+ const schema = {
177
+ page: {
178
+ default: 1,
179
+ parse: (value: string | string[] | undefined) =>
180
+ value ? Number.parseInt(String(value), 10) : 1,
181
+ },
182
+ limit: {
183
+ default: 20,
184
+ parse: (value: string | string[] | undefined) =>
185
+ value ? Number.parseInt(String(value), 10) : 20,
186
+ },
187
+ search: {
188
+ default: "",
189
+ },
190
+ };
191
+
192
+ const { result } = renderHook(() => useTypedSearchParams(schema));
193
+
194
+ expect(result.current).toEqual({
195
+ page: 2,
196
+ limit: 50,
197
+ search: "test",
198
+ });
199
+ });
200
+
201
+ it("should use defaults for missing params", () => {
202
+ mockUseParams.mockReturnValue({});
203
+
204
+ const schema = {
205
+ page: { default: 1 },
206
+ limit: { default: 20 },
207
+ search: { default: "" },
208
+ };
209
+
210
+ const { result } = renderHook(() => useTypedSearchParams(schema));
211
+
212
+ expect(result.current).toEqual({
213
+ page: 1,
214
+ limit: 20,
215
+ search: "",
216
+ });
217
+ });
218
+ });
219
+ });