use-prms 0.3.0 → 0.4.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.
package/README.md CHANGED
@@ -14,17 +14,19 @@ Type-safe URL-parameter (query and hash) management with minimal, human-readable
14
14
  - [Custom Params](#custom)
15
15
  - [Batch Updates](#batch)
16
16
  - [URL Encoding](#encoding)
17
+ - [Binary Encoding](#binary)
17
18
  - [Framework-Agnostic Core](#core)
18
19
  - [Hash Params](#hash)
19
20
  - [API Reference](#api)
20
21
  - [Examples](#examples)
22
+ - [Reverse Inspo](#reverse-inspo)
21
23
  - [License](#license)
22
24
 
23
25
  ## Features <a id="features"></a>
24
26
 
25
27
  - 🎯 **Type-safe**: Full TypeScript support with generic `Param<T>` interface
26
28
  - 📦 **Tiny URLs**: Smart encoding - omit defaults, use short keys, `+` for spaces
27
- - ⚛️ **React hooks**: `useUrlParam()` and `useUrlParams()` for seamless integration
29
+ - ⚛️ **React hooks**: `useUrlState()` and `useUrlStates()` for seamless integration
28
30
  - 🔧 **Framework-agnostic**: Core utilities work anywhere, React hooks are optional
29
31
  - 🌳 **Tree-shakeable**: ESM + CJS builds with TypeScript declarations
30
32
  - 0️⃣ **Zero dependencies**: Except React (peer dependency, optional)
@@ -44,12 +46,12 @@ pnpm add use-prms
44
46
  ## Quick Start <a id="quick-start"></a>
45
47
 
46
48
  ```typescript
47
- import { useUrlParam, boolParam, stringParam, intParam } from 'use-prms'
49
+ import { useUrlState, boolParam, stringParam, intParam } from 'use-prms'
48
50
 
49
51
  function MyComponent() {
50
- const [zoom, setZoom] = useUrlParam('z', boolParam)
51
- const [device, setDevice] = useUrlParam('d', stringParam())
52
- const [count, setCount] = useUrlParam('n', intParam(10))
52
+ const [zoom, setZoom] = useUrlState('z', boolParam)
53
+ const [device, setDevice] = useUrlState('d', stringParam())
54
+ const [count, setCount] = useUrlState('n', intParam(10))
53
55
 
54
56
  // URL: ?z&d=gym&n=5
55
57
  // zoom = true, device = "gym", count = 5
@@ -68,31 +70,31 @@ function MyComponent() {
68
70
 
69
71
  ### Boolean
70
72
  ```typescript
71
- const [enabled, setEnabled] = useUrlParam('e', boolParam)
73
+ const [enabled, setEnabled] = useUrlState('e', boolParam)
72
74
  // ?e → true
73
75
  // (absent) → false
74
76
  ```
75
77
 
76
78
  ### Strings
77
79
  ```typescript
78
- const [name, setName] = useUrlParam('n', stringParam()) // optional
79
- const [mode, setMode] = useUrlParam('m', defStringParam('auto')) // with default
80
+ const [name, setName] = useUrlState('n', stringParam()) // optional
81
+ const [mode, setMode] = useUrlState('m', defStringParam('auto')) // with default
80
82
  // ?n=foo → "foo"
81
83
  // (absent) → undefined / "auto"
82
84
  ```
83
85
 
84
86
  ### Numbers
85
87
  ```typescript
86
- const [count, setCount] = useUrlParam('c', intParam(0))
87
- const [ratio, setRatio] = useUrlParam('r', floatParam(1.0))
88
- const [id, setId] = useUrlParam('id', optIntParam) // number | null
88
+ const [count, setCount] = useUrlState('c', intParam(0))
89
+ const [ratio, setRatio] = useUrlState('r', floatParam(1.0))
90
+ const [id, setId] = useUrlState('id', optIntParam) // number | null
89
91
  // ?c=5&r=1.5&id=123 → 5, 1.5, 123
90
92
  // (absent) → 0, 1.0, null
91
93
  ```
92
94
 
93
95
  ### Enums
94
96
  ```typescript
95
- const [theme, setTheme] = useUrlParam(
97
+ const [theme, setTheme] = useUrlState(
96
98
  't',
97
99
  enumParam('light', ['light', 'dark', 'auto'] as const)
98
100
  )
@@ -102,20 +104,20 @@ const [theme, setTheme] = useUrlParam(
102
104
 
103
105
  ### Arrays (delimiter-separated)
104
106
  ```typescript
105
- const [tags, setTags] = useUrlParam('tags', stringsParam([], ','))
106
- const [ids, setIds] = useUrlParam('ids', numberArrayParam([]))
107
+ const [tags, setTags] = useUrlState('tags', stringsParam([], ','))
108
+ const [ids, setIds] = useUrlState('ids', numberArrayParam([]))
107
109
  // ?tags=foo,bar,baz → ["foo", "bar", "baz"]
108
110
  // ?ids=1,2,3 → [1, 2, 3]
109
111
  ```
110
112
 
111
113
  ### Multi-value Arrays (repeated keys)
112
114
  ```typescript
113
- import { useMultiUrlParam, multiStringParam, multiIntParam } from 'use-prms'
115
+ import { useMultiUrlState, multiStringParam, multiIntParam } from 'use-prms'
114
116
 
115
- const [tags, setTags] = useMultiUrlParam('tag', multiStringParam())
117
+ const [tags, setTags] = useMultiUrlState('tag', multiStringParam())
116
118
  // ?tag=foo&tag=bar&tag=baz → ["foo", "bar", "baz"]
117
119
 
118
- const [ids, setIds] = useMultiUrlParam('id', multiIntParam())
120
+ const [ids, setIds] = useMultiUrlState('id', multiIntParam())
119
121
  // ?id=1&id=2&id=3 → [1, 2, 3]
120
122
 
121
123
  // Also available: multiFloatParam()
@@ -124,14 +126,14 @@ const [ids, setIds] = useMultiUrlParam('id', multiIntParam())
124
126
  ### Compact Code Mapping
125
127
  ```typescript
126
128
  // Single value with short codes
127
- const [metric, setMetric] = useUrlParam('y', codeParam('Rides', {
129
+ const [metric, setMetric] = useUrlState('y', codeParam('Rides', {
128
130
  Rides: 'r',
129
131
  Minutes: 'm',
130
132
  }))
131
133
  // ?y=m → "Minutes", omitted for default "Rides"
132
134
 
133
135
  // Multi-value with short codes (omits when all selected)
134
- const [regions, setRegions] = useUrlParam('r', codesParam(
136
+ const [regions, setRegions] = useUrlState('r', codesParam(
135
137
  ['NYC', 'JC', 'HOB'],
136
138
  { NYC: 'n', JC: 'j', HOB: 'h' }
137
139
  ))
@@ -140,7 +142,7 @@ const [regions, setRegions] = useUrlParam('r', codesParam(
140
142
 
141
143
  ### Pagination
142
144
  ```typescript
143
- const [page, setPage] = useUrlParam('p', paginationParam(20))
145
+ const [page, setPage] = useUrlState('p', paginationParam(20))
144
146
  // Encodes offset + pageSize compactly using + as delimiter:
145
147
  // { offset: 0, pageSize: 20 } → (omitted)
146
148
  // { offset: 0, pageSize: 50 } → ?p=+50
@@ -172,18 +174,18 @@ const dateParam: Param<Date> = {
172
174
  }
173
175
  }
174
176
 
175
- const [date, setDate] = useUrlParam('d', dateParam)
177
+ const [date, setDate] = useUrlState('d', dateParam)
176
178
  // ?d=251123 → Date(2025, 10, 23)
177
179
  ```
178
180
 
179
181
  ## Batch Updates <a id="batch"></a>
180
182
 
181
- Use `useUrlParams()` to update multiple parameters atomically:
183
+ Use `useUrlStates()` to update multiple parameters atomically:
182
184
 
183
185
  ```typescript
184
- import { useUrlParams, intParam, boolParam } from 'use-prms'
186
+ import { useUrlStates, intParam, boolParam } from 'use-prms'
185
187
 
186
- const { values, setValues } = useUrlParams({
188
+ const { values, setValues } = useUrlStates({
187
189
  page: intParam(1),
188
190
  size: intParam(20),
189
191
  grid: boolParam
@@ -202,11 +204,72 @@ setValues({ page: 2, size: 50 })
202
204
 
203
205
  Example:
204
206
  ```typescript
205
- const [devices, setDevices] = useUrlParam('d', stringsParam([], ' '))
207
+ const [devices, setDevices] = useUrlState('d', stringsParam([], ' '))
206
208
  setDevices(['gym', 'bedroom'])
207
209
  // URL: ?d=gym+bedroom
208
210
  ```
209
211
 
212
+ ## Binary Encoding <a id="binary"></a>
213
+
214
+ For complex data that doesn't fit well into string encoding, `use-prms` provides binary encoding utilities with URL-safe base64.
215
+
216
+ ### BitBuffer
217
+
218
+ Low-level bit packing for custom binary formats:
219
+
220
+ ```typescript
221
+ import { BitBuffer } from 'use-prms'
222
+
223
+ // Encoding
224
+ const buf = new BitBuffer()
225
+ buf.encodeInt(myEnum, 3) // 3 bits for enum (0-7)
226
+ buf.encodeInt(myCount, 8) // 8 bits for count (0-255)
227
+ buf.encodeBigInt(myId, 48) // 48 bits for ID
228
+ const urlParam = buf.toBase64()
229
+
230
+ // Decoding
231
+ const buf = BitBuffer.fromBase64(urlParam)
232
+ const myEnum = buf.decodeInt(3)
233
+ const myCount = buf.decodeInt(8)
234
+ const myId = buf.decodeBigInt(48)
235
+ ```
236
+
237
+ ### Float Params
238
+
239
+ Encode floats compactly as base64:
240
+
241
+ ```typescript
242
+ import { floatParam } from 'use-prms'
243
+
244
+ // Lossless (11 chars, exact IEEE 754)
245
+ const [zoom, setZoom] = useUrlState('z', floatParam(1.0))
246
+
247
+ // Lossy (fewer chars, configurable precision)
248
+ const [lat, setLat] = useUrlState('lat', floatParam({
249
+ default: 0,
250
+ exp: 5, // exponent bits
251
+ mant: 22, // mantissa bits (~7 decimal digits)
252
+ }))
253
+ ```
254
+
255
+ ### Custom Alphabets
256
+
257
+ Choose between standard base64url or ASCII-sorted alphabet:
258
+
259
+ ```typescript
260
+ import { ALPHABETS, binaryParam, floatParam } from 'use-prms'
261
+
262
+ // Standard RFC 4648 (default)
263
+ // ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_
264
+
265
+ // ASCII-sorted (lexicographic sort = numeric sort)
266
+ // -0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz
267
+
268
+ const param = floatParam({ default: 0, alphabet: 'sortable' })
269
+ ```
270
+
271
+ The `sortable` alphabet is useful when encoded strings need to sort in the same order as their numeric values (e.g., for database indexing).
272
+
210
273
  ## Framework-Agnostic Core <a id="core"></a>
211
274
 
212
275
  Use the core utilities without React:
@@ -229,9 +292,9 @@ Use hash fragment (`#key=value`) instead of query string (`?key=value`):
229
292
 
230
293
  ```typescript
231
294
  // Just change the import path
232
- import { useUrlParam, boolParam } from 'use-prms/hash'
295
+ import { useUrlState, boolParam } from 'use-prms/hash'
233
296
 
234
- const [zoom, setZoom] = useUrlParam('z', boolParam)
297
+ const [zoom, setZoom] = useUrlState('z', boolParam)
235
298
  // URL: https://example.com/#z (instead of ?z)
236
299
  ```
237
300
 
@@ -239,7 +302,7 @@ Same API, different URL location. Useful when query strings conflict with server
239
302
 
240
303
  ## API Reference <a id="api"></a>
241
304
 
242
- ### `useUrlParam<T>(key: string, param: Param<T>, push?: boolean)`
305
+ ### `useUrlState<T>(key: string, param: Param<T>, push?: boolean)`
243
306
 
244
307
  React hook for managing a single URL parameter.
245
308
 
@@ -248,7 +311,7 @@ React hook for managing a single URL parameter.
248
311
  - `push`: Use pushState (true) or replaceState (false, default)
249
312
  - Returns: `[value: T, setValue: (value: T) => void]`
250
313
 
251
- ### `useUrlParams<P>(params: P, push?: boolean)`
314
+ ### `useUrlStates<P>(params: P, push?: boolean)`
252
315
 
253
316
  React hook for managing multiple URL parameters together.
254
317
 
@@ -256,7 +319,7 @@ React hook for managing multiple URL parameters together.
256
319
  - `push`: Use pushState (true) or replaceState (false, default)
257
320
  - Returns: `{ values, setValues }`
258
321
 
259
- ### `useMultiUrlParam<T>(key: string, param: MultiParam<T>, push?: boolean)`
322
+ ### `useMultiUrlState<T>(key: string, param: MultiParam<T>, push?: boolean)`
260
323
 
261
324
  React hook for managing a multi-value URL parameter (repeated keys).
262
325
 
@@ -312,6 +375,17 @@ type MultiParam<T> = {
312
375
  | `multiIntParam(init?)` | `MultiParam<number[]>` | Repeated integer params |
313
376
  | `multiFloatParam(init?)` | `MultiParam<number[]>` | Repeated float params |
314
377
 
378
+ ### Binary Encoding
379
+
380
+ | Export | Description |
381
+ |--------|-------------|
382
+ | `BitBuffer` | Bit-level buffer for packing/unpacking arbitrary bit widths |
383
+ | `binaryParam(opts)` | Create param from `toBytes`/`fromBytes` converters |
384
+ | `base64Param(toBytes, fromBytes)` | Shorthand for `binaryParam` |
385
+ | `base64Encode(bytes, opts?)` | Encode `Uint8Array` to base64 string |
386
+ | `base64Decode(str, opts?)` | Decode base64 string to `Uint8Array` |
387
+ | `ALPHABETS` | Preset alphabets: `rfc4648` (default), `sortable` (ASCII-ordered) |
388
+
315
389
  ### Core Utilities
316
390
 
317
391
  - `serializeParams(params)`: Convert params object to URL query string *(deprecated, use `serializeMultiParams`)*
@@ -349,6 +423,107 @@ Projects using `use-prms`:
349
423
  [use-kbd-gh]: https://github.com/runsascoded/use-kbd
350
424
  [use-kbd-search]: https://github.com/search?q=repo%3Arunsascoded%2Fuse-kbd+use-prms&type=code
351
425
 
426
+ ## Reverse Inspo <a id="reverse-inspo"></a>
427
+
428
+ It's nice when URLs are concise but also reasonably human-readable. Some examples I've seen in the wild that exhibit room for improvement:
429
+
430
+ ### UUID Soup (OpenAI Careers)
431
+ ```
432
+ https://openai.com/careers/search/
433
+ ?l=e8062547-b090-4206-8f1e-7329e0014e98%2C07ed9191-5bc6-421b-9883-f1ac2e276ad7
434
+ &c=e1e973fe-6f0a-475f-9361-a9b6c095d869%2Cf002fe09-4cec-46b0-8add-8bf9ff438a62
435
+ %2Cab2b9da4-24a4-47df-8bed-1ed5a39c7036%2C687d87ec-1505-40e7-a2b5-cc7f31c0ea48
436
+ %2Cd36236ec-fb74-49bd-bd3f-9d8365e2e2cb%2C27c9a852-c401-450e-9480-d3b507b8f64a
437
+ %2C6dd4a467-446d-4093-8d57-d4633a571123%2C7cba3ac0-2b6e-4d52-ad38-e39a5f61c73f
438
+ %2C0f06f916-a404-414f-813f-6ac7ff781c61%2Cfb2b77c5-5f20-4a93-a1c4-c3d640d88e04
439
+ ```
440
+ 12 UUIDs for location and category filters. Each UUID is 36 characters. With short codes, this could be `?l=sf,ny&c=eng,res,des,acct,data,hr,infra,accel,acq,bus`.
441
+
442
+ ### Encrypted Blobs (Supercast, Priceline)
443
+ ```
444
+ https://feeds.supercast.com/episodes/8a1aa9e2dde4319825e6a8171b4d51fa1835ef4a
445
+ 6730170db60a92c8f0670bb08c3cef884f0e4288c970c980083820e89cd692f582c44cde
446
+ 544c7aae86fc721f69ed9f695a43e5e21f4d344b32e70bae48a8fe0ae8b472d99502041a
447
+ bad3dc650a6973653c094eae0631f637d96bb42ab5d26b8ea6b1638b7ffa23f66e46282b
448
+ 52970b59b2c13f9e6214251ad793be244bb9dc7e5bd7cefe77b6ec71b06c85e3bc9c194a
449
+ d4ca10b27cfd7b8b1c181b3d9aea144bb978d1d790f08d89049d5a29a477651f1b799eec
450
+ 827ed95209dc741207e2b331170cb01c625d51982913eb8757ef2b2037235624a7bbfab9
451
+ 8a641e98a507ee096d0678c8ab458fd87731a9a7a0bdc87a99fbbfe684be10f5d4259265
452
+ 68b041a308017ce2901b3c6bf4b3bc89a2b13f3c54047d2fc5f69e9a5053b5e5bb2e0f70
453
+ a2a77d9a25c97b890faec970e29f1c6961b1e00ccd1d8ba9c4006ba8b657193fe5a5b8e4
454
+ 6aa6a86492c381c79afe09d347d25c550c195d080695e3b97c012be3ebf1e2e64bd9f6c2
455
+ 9977e4b34184858bcf99164010dc3746f49d90df559f7dfa6f029f50f35f7777c44d1247
456
+ ecdfc7861969f172d63eb3acc620ac25919cdc5caf4397793b7d564ccc4b0519118027.mp3
457
+ ?key=8kSKDMBUEi2TCGyzhdzZBVSN&v=0
458
+ ```
459
+
460
+ ```
461
+ https://www.priceline.com/relax/at/2003205/from/20240628/to/20240629/rooms/1
462
+ ?meta-id=AyOy_-ov9Edvq6cYGWUbaO9KdvlksSZCnHtEiIUqbvfIqUNLp0ZV0WiDB-MXSyZhxM
463
+ mSw6xJm0HTePNNo_NwvV_Mzo1DeUvJhE53dMnjIqnwb7rfeGGSHOuOML_0zcWCYppfcv6Cf8T
464
+ Na_TIadYlC8PJkvC_qY7bm0lXIqygsn03MyXPyXyUCXNRcKiIm2QS5bWoOeiO48zWgHRtLUDm
465
+ cNx8o6rdlIukl18vqu8RQYajSd3Yt9bbWwDTBjeEduJ2sfoh4Mi3XtGzbqy8YpUrRgIUCGCYf
466
+ DHBdaS47dUkqKfqtQvY7yCPh9Y4YNUZtt9w-TRqndd6AdvbOMprSAbawg8IU5wIj-yEbZr82e
467
+ CcQg2dylETYccSaRK07WHSEJx7
468
+ &pclnId=0571D9ABC99167E702D55CD454625E1BD51BC6742D4EB3A6869799404CB9B21E0E31
469
+ CA463BDC3DE5A56EDB9C6B55C3F06EB5CBBC77502608C5279D0943A5F2545B3F0E4366F3FB
470
+ CCDE32424FB9D2CC10B7E2B68DD59C89151023C9B800744FDDF1C7D85AEB2CF27E
471
+ &gid=5369&cityId=3000035889&cur=USD&backlink-id=gotjhpxt5bp
472
+ ```
473
+ 900 hex characters, 400-char tracking IDs, session blobs.
474
+
475
+ ### Tracking Parameter Avalanche (Apple TV)
476
+ ```
477
+ https://tv.apple.com/us/show/severance/umc.cmc.1srk2goyh2q2zdxcx605w8vtx
478
+ ?ign-itscg=MC_20000&ign-itsct=atvp_brand_omd
479
+ &mttn3pid=Google%20AdWords&mttnagencyid=a5e&mttncc=US
480
+ &mttnsiteid=143238&mttnsubad=OUS2019927_1-592764821446-m
481
+ &mttnsubkw=133111427260__zxnj5jSX_&mttnsubplmnt=
482
+ ```
483
+ Seven `mttn*` tracking parameters that are excessively verbose (and come from a single ad click).
484
+
485
+ ### Base64-Encoded Redirect URLs (Wired)
486
+ ```
487
+ https://link.wired.com/external/39532383.1121/aHR0cHM6Ly9jb25kZW5hc3Quem9vbS
488
+ 51cy93ZWJpbmFyL3JlZ2lzdGVyL1dOX29kcldRdE5uUkdhSUN3MHZob0N3ckE_dXRtX3Nvd
489
+ XJjZT1ubCZ1dG1fYnJhbmQ9d2lyZWQmdXRtX21haWxpbmc9V0lSX1BheXdhbGxTdWJzXzA0
490
+ MjMyNV9TcGVjaWFsX0FJVW5sb2NrZWRfTkxTVUJTSW52aXRlJnV0bV9jYW1wYWlnbj1hdWQ
491
+ tZGV2JnV0bV9tZWRpdW09ZW1haWwmdXRtX2NvbnRlbnQ9V0lSX1BheXdhbGxTdWJzXzA0Mj
492
+ MyNV9TcGVjaWFsX0FJVW5sb2NrZWRfTkxTVUJTSW52aXRlJmJ4aWQ9NWNjOWUwZjdmYzk0M
493
+ mQxM2ViMWY0YjhjJmNuZGlkPTUwNTQyMzY4Jmhhc2hhPTQwODY5ZjRmY2ExOWRkZjU2NTUz
494
+ M2Q2NzMxYmVkMTExJmhhc2hiPWFjNzQxNjk4NjkyMTE1YWExOGRkNzg5N2JjMTIxNmIwNWM
495
+ 0YmI2ODgmaGFzaGM9ZTA5YTA4NzM0MTM3NDA4ODE3NzZlNjExNzQ3NzQ3NDM5ZDYzMGM2YT
496
+ k0NGVmYTIwOGFhMzhhYTMwZjljYTE0NyZlc3JjPU9JRENfU0VMRUNUX0FDQ09VTlRfUEFHR
497
+ Q/5cc9e0f7fc942d13eb1f4b8cB8513f7ce
498
+ ```
499
+ A URL containing another (base64-encoded) URL containing UTM params, hashes, and tracking IDs.
500
+
501
+ ### Kitchen Sink (Grubhub)
502
+ ```
503
+ https://www.grubhub.com/restaurant/bobs-noodle-house-123-main-st-newark/4857291
504
+ /grouporder/Xk7rPwchQfDsT3J9yCtghR
505
+ ?pageNum=1&pageSize=20
506
+ &facet=scheduled%3Afalse&facet=orderType%3AALL
507
+ &includePartnerOrders=true&sorts=default&blockModal=true
508
+ &utm_source=grubhub_web&utm_medium=content_owned
509
+ &utm_campaign=product_sharedcart_join&utm_content=share-link
510
+ ```
511
+ Session IDs, pagination defaults that could be omitted, boolean flags, four UTM parameters, and all more verbose than necessary, resulting in an unwieldy URL.
512
+
513
+ ### The `use-prms` way
514
+
515
+ This may not be best in all cases, but `use-prms` encourages encoding the same information more compactly:
516
+
517
+ | Verbose | Compact | Meaning |
518
+ |----------------------------------------|----------------------------------|--------------------------------------|
519
+ | `?show_grid=true` | `?g` | Boolean flag |
520
+ | `?page_number=5&page_size=50` | `?p=5x50` | Compact, combined state |
521
+ | `?page_number=5&page_size=20` | `?p=5` | Default values omitted |
522
+ | `?category=e1e973fe-6f0a-...` | `?c=eng` | Short, human-readable codes for enums |
523
+ | `?latitude=40.7128&longitude=-74.0060` | `?ll=40.7128-74.0060` | Compact, combined state |
524
+
525
+ URLs are part of your UI. Treat them with the same care as your design.
526
+
352
527
  ## License <a id="license"></a>
353
528
 
354
529
  MIT