use-prms
v0.4.1
Published
Type-safe URL-parameter (query and hash) management with minimal, human-readable encoding and decoding
Maintainers
Readme
use-prms
Type-safe URL-parameter (query and hash) management with minimal, human-readable encoding and decoding.
- Features
- Installation
- Quick Start
- Built-in Param Types
- Custom Params
- Batch Updates
- URL Encoding
- Binary Encoding
- Framework-Agnostic Core
- Hash Params
- API Reference
- Examples
- Reverse Inspo
- License
Features
- 🎯 Type-safe: Full TypeScript support with generic
Param<T>interface - 📦 Tiny URLs: Smart encoding - omit defaults, use short keys,
+for spaces - ⚛️ React hooks:
useUrlState()anduseUrlStates()for seamless integration - 🔧 Framework-agnostic: Core utilities work anywhere, React hooks are optional
- 🌳 Tree-shakeable: ESM + CJS builds with TypeScript declarations
- 0️⃣ Zero dependencies: Except React (peer dependency, optional)
- 🔁 Multi-value params: Support for repeated keys like
?tag=a&tag=b - #️⃣ Hash params: Use hash fragment (
#key=value) instead of query string
Installation
npm install use-prmsOr:
pnpm add use-prmsQuick Start
import { useUrlState, boolParam, stringParam, intParam } from 'use-prms'
function MyComponent() {
const [zoom, setZoom] = useUrlState('z', boolParam)
const [device, setDevice] = useUrlState('d', stringParam())
const [count, setCount] = useUrlState('n', intParam(10))
// URL: ?z&d=gym&n=5
// zoom = true, device = "gym", count = 5
return (
<div>
<button onClick={() => setZoom(!zoom)}>Toggle Zoom</button>
<input value={device ?? ''} onChange={e => setDevice(e.target.value)} />
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
</div>
)
}Built-in Param Types
Boolean
const [enabled, setEnabled] = useUrlState('e', boolParam)
// ?e → true
// (absent) → falseStrings
const [name, setName] = useUrlState('n', stringParam()) // optional
const [mode, setMode] = useUrlState('m', defStringParam('auto')) // with default
// ?n=foo → "foo"
// (absent) → undefined / "auto"Numbers
const [count, setCount] = useUrlState('c', intParam(0))
const [ratio, setRatio] = useUrlState('r', floatParam(1.0))
const [id, setId] = useUrlState('id', optIntParam) // number | null
// ?c=5&r=1.5&id=123 → 5, 1.5, 123
// (absent) → 0, 1.0, nullEnums
const [theme, setTheme] = useUrlState(
't',
enumParam('light', ['light', 'dark', 'auto'] as const)
)
// ?t=dark → "dark"
// ?t=invalid → "light" (warns in console)Arrays (delimiter-separated)
const [tags, setTags] = useUrlState('tags', stringsParam([], ','))
const [ids, setIds] = useUrlState('ids', numberArrayParam([]))
// ?tags=foo,bar,baz → ["foo", "bar", "baz"]
// ?ids=1,2,3 → [1, 2, 3]Multi-value Arrays (repeated keys)
import { useMultiUrlState, multiStringParam, multiIntParam } from 'use-prms'
const [tags, setTags] = useMultiUrlState('tag', multiStringParam())
// ?tag=foo&tag=bar&tag=baz → ["foo", "bar", "baz"]
const [ids, setIds] = useMultiUrlState('id', multiIntParam())
// ?id=1&id=2&id=3 → [1, 2, 3]
// Also available: multiFloatParam()Compact Code Mapping
// Single value with short codes
const [metric, setMetric] = useUrlState('y', codeParam('Rides', {
Rides: 'r',
Minutes: 'm',
}))
// ?y=m → "Minutes", omitted for default "Rides"
// Multi-value with short codes (omits when all selected)
const [regions, setRegions] = useUrlState('r', codesParam(
['NYC', 'JC', 'HOB'],
{ NYC: 'n', JC: 'j', HOB: 'h' }
))
// ?r=nj → ["NYC", "JC"], omitted when all three selectedPagination
const [page, setPage] = useUrlState('p', paginationParam(20))
// Encodes offset + pageSize compactly using + as delimiter:
// { offset: 0, pageSize: 20 } → (omitted)
// { offset: 0, pageSize: 50 } → ?p=+50
// { offset: 100, pageSize: 20 } → ?p=100
// { offset: 100, pageSize: 50 } → ?p=100+50Custom Params
Create your own param encoders/decoders:
import type { Param } from 'use-prms'
// Example: Compact date encoding (YYMMDD)
const dateParam: Param<Date> = {
encode: (date) => {
const yy = String(date.getFullYear()).slice(-2)
const mm = String(date.getMonth() + 1).padStart(2, '0')
const dd = String(date.getDate()).padStart(2, '0')
return `${yy}${mm}${dd}`
},
decode: (str) => {
if (!str || str.length !== 6) return new Date()
const yy = parseInt('20' + str.slice(0, 2), 10)
const mm = parseInt(str.slice(2, 4), 10) - 1
const dd = parseInt(str.slice(4, 6), 10)
return new Date(yy, mm, dd)
}
}
const [date, setDate] = useUrlState('d', dateParam)
// ?d=251123 → Date(2025, 10, 23)Batch Updates
Use useUrlStates() to update multiple parameters atomically:
import { useUrlStates, intParam, boolParam } from 'use-prms'
const { values, setValues } = useUrlStates({
page: intParam(1),
size: intParam(20),
grid: boolParam
})
// Update multiple params at once (single history entry)
setValues({ page: 2, size: 50 })URL Encoding
- Spaces: Encoded as
+(standard form-urlencoded) - Defaults: Omitted from URL (keeps URLs minimal)
- Booleans: Present = true (
?z), absent = false - Empty values: Valueless params (
?keywithout=)
Example:
const [devices, setDevices] = useUrlState('d', stringsParam([], ' '))
setDevices(['gym', 'bedroom'])
// URL: ?d=gym+bedroomBinary Encoding
For complex data that doesn't fit well into string encoding, use-prms provides binary encoding utilities with URL-safe base64.
BitBuffer
Low-level bit packing for custom binary formats:
import { BitBuffer } from 'use-prms'
// Encoding
const buf = new BitBuffer()
buf.encodeInt(myEnum, 3) // 3 bits for enum (0-7)
buf.encodeInt(myCount, 8) // 8 bits for count (0-255)
buf.encodeBigInt(myId, 48) // 48 bits for ID
const urlParam = buf.toBase64()
// Decoding
const buf = BitBuffer.fromBase64(urlParam)
const myEnum = buf.decodeInt(3)
const myCount = buf.decodeInt(8)
const myId = buf.decodeBigInt(48)Float Params
Encode floats compactly as base64:
import { floatParam } from 'use-prms'
// Lossless (11 chars, exact IEEE 754)
const [zoom, setZoom] = useUrlState('z', floatParam(1.0))
// Lossy (fewer chars, configurable precision)
const [lat, setLat] = useUrlState('lat', floatParam({
default: 0,
exp: 5, // exponent bits
mant: 22, // mantissa bits (~7 decimal digits)
}))Custom Alphabets
Choose between standard base64url or ASCII-sorted alphabet:
import { ALPHABETS, binaryParam, floatParam } from 'use-prms'
// Standard RFC 4648 (default)
// ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_
// ASCII-sorted (lexicographic sort = numeric sort)
// -0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz
const param = floatParam({ default: 0, alphabet: 'sortable' })The sortable alphabet is useful when encoded strings need to sort in the same order as their numeric values (e.g., for database indexing).
Framework-Agnostic Core
Use the core utilities without React:
import { boolParam, serializeMultiParams, parseMultiParams } from 'use-prms'
// Encode
const params = { z: [boolParam.encode(true) ?? ''], d: ['gym'] }
const search = serializeMultiParams(params) // "z&d=gym"
// Decode
const parsed = parseMultiParams(window.location.search)
const zoom = boolParam.decode(parsed.z?.[0]) // trueHash Params
Use hash fragment (#key=value) instead of query string (?key=value):
// Just change the import path
import { useUrlState, boolParam } from 'use-prms/hash'
const [zoom, setZoom] = useUrlState('z', boolParam)
// URL: https://example.com/#z (instead of ?z)Same API, different URL location. Useful when query strings conflict with server routing or you want params to survive page reloads without server involvement.
API Reference
useUrlState<T>(key: string, param: Param<T>, push?: boolean)
React hook for managing a single URL parameter.
key: Query parameter keyparam: Param encoder/decoderpush: Use pushState (true) or replaceState (false, default)- Returns:
[value: T, setValue: (value: T) => void]
useUrlStates<P>(params: P, push?: boolean)
React hook for managing multiple URL parameters together.
params: Object mapping keys to Param typespush: Use pushState (true) or replaceState (false, default)- Returns:
{ values, setValues }
useMultiUrlState<T>(key: string, param: MultiParam<T>, push?: boolean)
React hook for managing a multi-value URL parameter (repeated keys).
key: Query parameter keyparam: MultiParam encoder/decoderpush: Use pushState (true) or replaceState (false, default)- Returns:
[value: T, setValue: (value: T) => void]
Param<T>
Bidirectional encoder/decoder interface:
type Param<T> = {
encode: (value: T) => string | undefined
decode: (encoded: string | undefined) => T
}MultiParam<T>
Multi-value encoder/decoder interface:
type MultiParam<T> = {
encode: (value: T) => string[]
decode: (encoded: string[]) => T
}Built-in Param Types
| Param | Type | Description |
|-------|------|-------------|
| boolParam | Param<boolean> | ?key = true, absent = false |
| stringParam(init?) | Param<string \| undefined> | Optional string |
| defStringParam(init) | Param<string> | Required string with default |
| intParam(init) | Param<number> | Integer with default |
| floatParam(init) | Param<number> | Float with default |
| optIntParam | Param<number \| null> | Optional integer |
| enumParam(init, values) | Param<T> | Validated enum |
| stringsParam(init?, delim?) | Param<string[]> | Delimiter-separated strings |
| numberArrayParam(init?) | Param<number[]> | Comma-separated numbers |
| codeParam(init, codeMap) | Param<T> | Enum with short URL codes |
| codesParam(allValues, codeMap, sep?) | Param<T[]> | Multi-value with short codes |
| paginationParam(defaultSize, validSizes?) | Param<Pagination> | Offset + page size |
Built-in MultiParam Types
| Param | Type | Description |
|-------|------|-------------|
| multiStringParam(init?) | MultiParam<string[]> | Repeated string params |
| multiIntParam(init?) | MultiParam<number[]> | Repeated integer params |
| multiFloatParam(init?) | MultiParam<number[]> | Repeated float params |
Binary Encoding
| Export | Description |
|--------|-------------|
| BitBuffer | Bit-level buffer for packing/unpacking arbitrary bit widths |
| binaryParam(opts) | Create param from toBytes/fromBytes converters |
| base64Param(toBytes, fromBytes) | Shorthand for binaryParam |
| base64Encode(bytes, opts?) | Encode Uint8Array to base64 string |
| base64Decode(str, opts?) | Decode base64 string to Uint8Array |
| ALPHABETS | Preset alphabets: rfc4648 (default), sortable (ASCII-ordered) |
Core Utilities
serializeParams(params): Convert params object to URL query string (deprecated, useserializeMultiParams)parseParams(source): Parse URL string or URLSearchParams to object (deprecated, useparseMultiParams)serializeMultiParams(params): Convert multi-value params to URL query stringparseMultiParams(source): Parse URL to multi-value params objectgetCurrentParams(): Get current URL params (browser only)updateUrl(params, push?): Update URL without reloading (browser only)
Examples
Projects using use-prms:
awair.runsascoded.com – Air quality dashboard (GitHub, usage)
Example:
?d=+br&y=thZ&t=-3dd=+br: devices (leading space = "include default")y=thZ: Y-axes configt=-3d: time range
kbd.rbw.sh – Keyboard shortcut manager demo site (GitHub, usage)
Reverse Inspo
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:
UUID Soup (OpenAI Careers)
https://openai.com/careers/search/
?l=e8062547-b090-4206-8f1e-7329e0014e98%2C07ed9191-5bc6-421b-9883-f1ac2e276ad7
&c=e1e973fe-6f0a-475f-9361-a9b6c095d869%2Cf002fe09-4cec-46b0-8add-8bf9ff438a62
%2Cab2b9da4-24a4-47df-8bed-1ed5a39c7036%2C687d87ec-1505-40e7-a2b5-cc7f31c0ea48
%2Cd36236ec-fb74-49bd-bd3f-9d8365e2e2cb%2C27c9a852-c401-450e-9480-d3b507b8f64a
%2C6dd4a467-446d-4093-8d57-d4633a571123%2C7cba3ac0-2b6e-4d52-ad38-e39a5f61c73f
%2C0f06f916-a404-414f-813f-6ac7ff781c61%2Cfb2b77c5-5f20-4a93-a1c4-c3d640d88e0412 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.
Encrypted Blobs (Supercast, Priceline)
https://feeds.supercast.com/episodes/8a1aa9e2dde4319825e6a8171b4d51fa1835ef4a
6730170db60a92c8f0670bb08c3cef884f0e4288c970c980083820e89cd692f582c44cde
544c7aae86fc721f69ed9f695a43e5e21f4d344b32e70bae48a8fe0ae8b472d99502041a
bad3dc650a6973653c094eae0631f637d96bb42ab5d26b8ea6b1638b7ffa23f66e46282b
52970b59b2c13f9e6214251ad793be244bb9dc7e5bd7cefe77b6ec71b06c85e3bc9c194a
d4ca10b27cfd7b8b1c181b3d9aea144bb978d1d790f08d89049d5a29a477651f1b799eec
827ed95209dc741207e2b331170cb01c625d51982913eb8757ef2b2037235624a7bbfab9
8a641e98a507ee096d0678c8ab458fd87731a9a7a0bdc87a99fbbfe684be10f5d4259265
68b041a308017ce2901b3c6bf4b3bc89a2b13f3c54047d2fc5f69e9a5053b5e5bb2e0f70
a2a77d9a25c97b890faec970e29f1c6961b1e00ccd1d8ba9c4006ba8b657193fe5a5b8e4
6aa6a86492c381c79afe09d347d25c550c195d080695e3b97c012be3ebf1e2e64bd9f6c2
9977e4b34184858bcf99164010dc3746f49d90df559f7dfa6f029f50f35f7777c44d1247
ecdfc7861969f172d63eb3acc620ac25919cdc5caf4397793b7d564ccc4b0519118027.mp3
?key=8kSKDMBUEi2TCGyzhdzZBVSN&v=0https://www.priceline.com/relax/at/2003205/from/20240628/to/20240629/rooms/1
?meta-id=AyOy_-ov9Edvq6cYGWUbaO9KdvlksSZCnHtEiIUqbvfIqUNLp0ZV0WiDB-MXSyZhxM
mSw6xJm0HTePNNo_NwvV_Mzo1DeUvJhE53dMnjIqnwb7rfeGGSHOuOML_0zcWCYppfcv6Cf8T
Na_TIadYlC8PJkvC_qY7bm0lXIqygsn03MyXPyXyUCXNRcKiIm2QS5bWoOeiO48zWgHRtLUDm
cNx8o6rdlIukl18vqu8RQYajSd3Yt9bbWwDTBjeEduJ2sfoh4Mi3XtGzbqy8YpUrRgIUCGCYf
DHBdaS47dUkqKfqtQvY7yCPh9Y4YNUZtt9w-TRqndd6AdvbOMprSAbawg8IU5wIj-yEbZr82e
CcQg2dylETYccSaRK07WHSEJx7
&pclnId=0571D9ABC99167E702D55CD454625E1BD51BC6742D4EB3A6869799404CB9B21E0E31
CA463BDC3DE5A56EDB9C6B55C3F06EB5CBBC77502608C5279D0943A5F2545B3F0E4366F3FB
CCDE32424FB9D2CC10B7E2B68DD59C89151023C9B800744FDDF1C7D85AEB2CF27E
&gid=5369&cityId=3000035889&cur=USD&backlink-id=gotjhpxt5bp900 hex characters, 400-char tracking IDs, session blobs.
Tracking Parameter Avalanche (Apple TV)
https://tv.apple.com/us/show/severance/umc.cmc.1srk2goyh2q2zdxcx605w8vtx
?ign-itscg=MC_20000&ign-itsct=atvp_brand_omd
&mttn3pid=Google%20AdWords&mttnagencyid=a5e&mttncc=US
&mttnsiteid=143238&mttnsubad=OUS2019927_1-592764821446-m
&mttnsubkw=133111427260__zxnj5jSX_&mttnsubplmnt=Seven mttn* tracking parameters that are excessively verbose (and come from a single ad click).
Base64-Encoded Redirect URLs (Wired)
https://link.wired.com/external/39532383.1121/aHR0cHM6Ly9jb25kZW5hc3Quem9vbS
51cy93ZWJpbmFyL3JlZ2lzdGVyL1dOX29kcldRdE5uUkdhSUN3MHZob0N3ckE_dXRtX3Nvd
XJjZT1ubCZ1dG1fYnJhbmQ9d2lyZWQmdXRtX21haWxpbmc9V0lSX1BheXdhbGxTdWJzXzA0
MjMyNV9TcGVjaWFsX0FJVW5sb2NrZWRfTkxTVUJTSW52aXRlJnV0bV9jYW1wYWlnbj1hdWQ
tZGV2JnV0bV9tZWRpdW09ZW1haWwmdXRtX2NvbnRlbnQ9V0lSX1BheXdhbGxTdWJzXzA0Mj
MyNV9TcGVjaWFsX0FJVW5sb2NrZWRfTkxTVUJTSW52aXRlJmJ4aWQ9NWNjOWUwZjdmYzk0M
mQxM2ViMWY0YjhjJmNuZGlkPTUwNTQyMzY4Jmhhc2hhPTQwODY5ZjRmY2ExOWRkZjU2NTUz
M2Q2NzMxYmVkMTExJmhhc2hiPWFjNzQxNjk4NjkyMTE1YWExOGRkNzg5N2JjMTIxNmIwNWM
0YmI2ODgmaGFzaGM9ZTA5YTA4NzM0MTM3NDA4ODE3NzZlNjExNzQ3NzQ3NDM5ZDYzMGM2YT
k0NGVmYTIwOGFhMzhhYTMwZjljYTE0NyZlc3JjPU9JRENfU0VMRUNUX0FDQ09VTlRfUEFHR
Q/5cc9e0f7fc942d13eb1f4b8cB8513f7ceA URL containing another (base64-encoded) URL containing UTM params, hashes, and tracking IDs.
Kitchen Sink (Grubhub)
https://www.grubhub.com/restaurant/bobs-noodle-house-123-main-st-newark/4857291
/grouporder/Xk7rPwchQfDsT3J9yCtghR
?pageNum=1&pageSize=20
&facet=scheduled%3Afalse&facet=orderType%3AALL
&includePartnerOrders=true&sorts=default&blockModal=true
&utm_source=grubhub_web&utm_medium=content_owned
&utm_campaign=product_sharedcart_join&utm_content=share-linkSession IDs, pagination defaults that could be omitted, boolean flags, four UTM parameters, and all more verbose than necessary, resulting in an unwieldy URL.
The use-prms way
This may not be best in all cases, but use-prms encourages encoding the same information more compactly:
| Verbose | Compact | Meaning |
|----------------------------------------|----------------------------------|--------------------------------------|
| ?show_grid=true | ?g | Boolean flag |
| ?page_number=5&page_size=50 | ?p=5x50 | Compact, combined state |
| ?page_number=5&page_size=20 | ?p=5 | Default values omitted |
| ?category=e1e973fe-6f0a-... | ?c=eng | Short, human-readable codes for enums |
| ?latitude=40.7128&longitude=-74.0060 | ?ll=40.7128-74.0060 | Compact, combined state |
URLs are part of your UI. Treat them with the same care as your design.
License
MIT
