Skip to content

React hooks for managing URL query parameters with type-safe encoding/decoding

Notifications You must be signed in to change notification settings

runsascoded/use-prms

Repository files navigation

use-prms

npm version license bundle size

Type-safe URL-parameter (query and hash) management with minimal, human-readable encoding and decoding.

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() and useUrlStates() 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-prms

Or:

pnpm add use-prms

Quick 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) → false

Strings

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, null

Enums

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 selected

Pagination

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+50

Custom 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 (?key without =)

Example:

const [devices, setDevices] = useUrlState('d', stringsParam([], ' '))
setDevices(['gym', 'bedroom'])
// URL: ?d=gym+bedroom

Binary 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])  // true

Hash 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 key
  • param: Param encoder/decoder
  • push: 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 types
  • push: 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 key
  • param: MultiParam encoder/decoder
  • push: 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, use serializeMultiParams)
  • parseParams(source): Parse URL string or URLSearchParams to object (deprecated, use parseMultiParams)
  • serializeMultiParams(params): Convert multi-value params to URL query string
  • parseMultiParams(source): Parse URL to multi-value params object
  • getCurrentParams(): Get current URL params (browser only)
  • updateUrl(params, push?): Update URL without reloading (browser only)

Examples

Projects using use-prms:

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-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.

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=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.

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/5cc9e0f7fc942d13eb1f4b8cB8513f7ce

A 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-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.

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

About

React hooks for managing URL query parameters with type-safe encoding/decoding

Resources

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •