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
- 🎯 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
npm install use-prmsOr:
pnpm add use-prmsimport { 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>
)
}const [enabled, setEnabled] = useUrlState('e', boolParam)
// ?e → true
// (absent) → falseconst [name, setName] = useUrlState('n', stringParam()) // optional
const [mode, setMode] = useUrlState('m', defStringParam('auto')) // with default
// ?n=foo → "foo"
// (absent) → undefined / "auto"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, nullconst [theme, setTheme] = useUrlState(
't',
enumParam('light', ['light', 'dark', 'auto'] as const)
)
// ?t=dark → "dark"
// ?t=invalid → "light" (warns in console)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]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()// 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 selectedconst [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+50Create 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)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 })- 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+bedroomFor complex data that doesn't fit well into string encoding, use-prms provides binary encoding utilities with URL-safe base64.
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)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)
}))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).
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]) // trueUse 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.
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]
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 }
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]
Bidirectional encoder/decoder interface:
type Param<T> = {
encode: (value: T) => string | undefined
decode: (encoded: string | undefined) => T
}Multi-value encoder/decoder interface:
type MultiParam<T> = {
encode: (value: T) => string[]
decode: (encoded: string[]) => T
}| 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 |
| Param | Type | Description |
|---|---|---|
multiStringParam(init?) |
MultiParam<string[]> |
Repeated string params |
multiIntParam(init?) |
MultiParam<number[]> |
Repeated integer params |
multiFloatParam(init?) |
MultiParam<number[]> |
Repeated float params |
| 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) |
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)
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)
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:
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-c3d640d88e04
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.
https://feeds.supercast.com/episodes/8a1aa9e2dde4319825e6a8171b4d51fa1835ef4a
6730170db60a92c8f0670bb08c3cef884f0e4288c970c980083820e89cd692f582c44cde
544c7aae86fc721f69ed9f695a43e5e21f4d344b32e70bae48a8fe0ae8b472d99502041a
bad3dc650a6973653c094eae0631f637d96bb42ab5d26b8ea6b1638b7ffa23f66e46282b
52970b59b2c13f9e6214251ad793be244bb9dc7e5bd7cefe77b6ec71b06c85e3bc9c194a
d4ca10b27cfd7b8b1c181b3d9aea144bb978d1d790f08d89049d5a29a477651f1b799eec
827ed95209dc741207e2b331170cb01c625d51982913eb8757ef2b2037235624a7bbfab9
8a641e98a507ee096d0678c8ab458fd87731a9a7a0bdc87a99fbbfe684be10f5d4259265
68b041a308017ce2901b3c6bf4b3bc89a2b13f3c54047d2fc5f69e9a5053b5e5bb2e0f70
a2a77d9a25c97b890faec970e29f1c6961b1e00ccd1d8ba9c4006ba8b657193fe5a5b8e4
6aa6a86492c381c79afe09d347d25c550c195d080695e3b97c012be3ebf1e2e64bd9f6c2
9977e4b34184858bcf99164010dc3746f49d90df559f7dfa6f029f50f35f7777c44d1247
ecdfc7861969f172d63eb3acc620ac25919cdc5caf4397793b7d564ccc4b0519118027.mp3
?key=8kSKDMBUEi2TCGyzhdzZBVSN&v=0
https://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=gotjhpxt5bp
900 hex characters, 400-char tracking IDs, session blobs.
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).
https://link.wired.com/external/39532383.1121/aHR0cHM6Ly9jb25kZW5hc3Quem9vbS
51cy93ZWJpbmFyL3JlZ2lzdGVyL1dOX29kcldRdE5uUkdhSUN3MHZob0N3ckE_dXRtX3Nvd
XJjZT1ubCZ1dG1fYnJhbmQ9d2lyZWQmdXRtX21haWxpbmc9V0lSX1BheXdhbGxTdWJzXzA0
MjMyNV9TcGVjaWFsX0FJVW5sb2NrZWRfTkxTVUJTSW52aXRlJnV0bV9jYW1wYWlnbj1hdWQ
tZGV2JnV0bV9tZWRpdW09ZW1haWwmdXRtX2NvbnRlbnQ9V0lSX1BheXdhbGxTdWJzXzA0Mj
MyNV9TcGVjaWFsX0FJVW5sb2NrZWRfTkxTVUJTSW52aXRlJmJ4aWQ9NWNjOWUwZjdmYzk0M
mQxM2ViMWY0YjhjJmNuZGlkPTUwNTQyMzY4Jmhhc2hhPTQwODY5ZjRmY2ExOWRkZjU2NTUz
M2Q2NzMxYmVkMTExJmhhc2hiPWFjNzQxNjk4NjkyMTE1YWExOGRkNzg5N2JjMTIxNmIwNWM
0YmI2ODgmaGFzaGM9ZTA5YTA4NzM0MTM3NDA4ODE3NzZlNjExNzQ3NzQ3NDM5ZDYzMGM2YT
k0NGVmYTIwOGFhMzhhYTMwZjljYTE0NyZlc3JjPU9JRENfU0VMRUNUX0FDQ09VTlRfUEFHR
Q/5cc9e0f7fc942d13eb1f4b8cB8513f7ce
A URL containing another (base64-encoded) URL containing UTM params, hashes, and tracking IDs.
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-link
Session IDs, pagination defaults that could be omitted, boolean flags, four UTM parameters, and all more verbose than necessary, resulting in an unwieldy URL.
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.
MIT