npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

preserve-search-params

v0.1.0

Published

Zero-dependency, framework-agnostic helpers to preserve URL search params across navigations and form submissions

Readme

preserve-search-params

The framework-agnostic primitive for preserving URL search params. Zero dependencies, sync, pure.

Most apps want one of the framework adapters built on top of this. Reach for the core directly when there's no adapter for your framework yet, or when you're composing with your own primitives.

Install

pnpm add preserve-search-params

Mental model

Input is a URLSearchParams. Output is a new URLSearchParams. Sync, pure, no I/O. You feed in the current URL's params, optionally narrow them down or set new values, and get back a copy ready to be turned into a query string.

preserveSearchParams(search, options?)

The four useful calls, in order:

Preserve everything (default)

import { preserveSearchParams } from 'preserve-search-params'

preserveSearchParams(new URLSearchParams('page=2&filter=active')).toString()
// → page=2&filter=active

Drop everything

For a reset link or button.

preserveSearchParams(new URLSearchParams('page=2&filter=active'), {
  preserve: [],
}).toString()
// → (empty string)

Allow-list specific params

Match is exact and case-sensitive.

preserveSearchParams(new URLSearchParams('page=2&filter=active&q=hello'), {
  preserve: ['page', 'q'],
}).toString()
// → page=2&q=hello

Set, override, or clear specific values

customValues runs after preservation. Setting a key to null clears it.

preserveSearchParams(new URLSearchParams('page=2&tab=info&drop=this'), {
  customValues: { tab: 'observations', drop: null, sort: 'name' },
}).toString()
// → page=2&tab=observations&sort=name

customValues is recursive

Nested objects and arrays serialize with bracket notation. Any depth works.

preserveSearchParams(new URLSearchParams(), {
  customValues: {
    filter: { status: 'active', tags: ['urgent', 'review'] },
    page: 2,
  },
}).toString()
// → filter%5Bstatus%5D=active&filter%5Btags%5D%5B%5D=urgent&filter%5Btags%5D%5B%5D=review&page=2

Decoded for readability:

filter[status]=active
filter[tags][]=urgent
filter[tags][]=review
page=2

Behavior summary:

  • Arrays of primitives use the bare-prefix form: tags[]=urgent&tags[]=review.
  • Arrays of objects use indexed bracket form: users[0][name]=Alice&users[1][name]=Bob.
  • Nested arrays under an already-bracketed prefix use indexed bracket form: filter[scores][0]=1.
  • Setting any nested key to null removes only that key. Setting an entire object subtree to null removes the whole subtree.

Composition recipes

These are the patterns the framework adapters layer on top of. If your framework doesn't have an adapter yet, use them directly.

Build a URL string

const search = preserveSearchParams(currentSearchParams, {
  customValues: { tab: 'observations' },
}).toString()
const href = `/items?${search}`

Imperative client navigation

Pass the result to whatever your framework's router expects.

const search = preserveSearchParams(currentSearchParams, {
  customValues: { page: null },
}).toString()
router.push(`/items?${search}`)

Server-side redirect

Read the request URL's searchParams, pipe through, return your framework's redirect.

const url = new URL(request.url)
const search = preserveSearchParams(url.searchParams, {
  customValues: { page: null },
}).toString()
return redirect(`/items?${search}`)

For the common case of merging an incoming request's params with a target path, the redirectPathWithSearchParams helper below removes the boilerplate.

redirectPathWithSearchParams(request, path, options?)

Builds a redirect destination string from a Request and a target path, preserving the request's search params and merging in any params already on the target path.

import { redirectPathWithSearchParams } from 'preserve-search-params'

// request.url = https://example.com/items?page=2&filter=active
const dest = redirectPathWithSearchParams(request, '/items/123#header', {
  customValues: { tab: 'observations' },
})
// → /items/123?page=2&filter=active&tab=observations#header
return redirect(dest)

Behavior:

  • The request's search params are preserved according to options.preserve (default 'all').
  • Search params already on path are merged in via customValues semantics — single-valued keys flow through as strings, repeated keys flow through as arrays (and serialize as key[]=...).
  • options.customValues overrides anything with the same key from the path.
  • The path's hash (#section) is preserved as-is.

Use it inside framework redirect helpers:

// React Router
return redirect(redirectPathWithSearchParams(request, '/items', { customValues: { page: null } }))

// Next.js Server Action — read the referer for the originating URL
const referer = (await headers()).get('referer') ?? 'http://x/'
redirect(redirectPathWithSearchParams(new Request(referer), '/items', { customValues: { page: null } }))

serializeToSearchParams(params, value, prefix)

The lower-level utility behind customValues. It mutates params in place by appending value under prefix, using the same recursive bracket notation.

import { serializeToSearchParams } from 'preserve-search-params'

const params = new URLSearchParams()
serializeToSearchParams(
  params,
  { status: 'active', tags: ['urgent'] },
  'filter'
)
params.toString()
// → filter%5Bstatus%5D=active&filter%5Btags%5D%5B%5D=urgent

Most users want preserveSearchParams. Reach for serializeToSearchParams when you want to append bracket-notation key/value pairs onto an existing URLSearchParams without going through the preservation step.

API reference

function preserveSearchParams(
  search: URLSearchParams,
  options?: SearchParamsPreserveOptions
): URLSearchParams

function redirectPathWithSearchParams(
  request: Request,
  path: string,
  options?: SearchParamsPreserveOptions
): string

function serializeToSearchParams(
  params: URLSearchParams,
  value: SearchParamsValue,
  prefix: string
): void

type SearchParamsPreserveOptions = {
  preserve?: 'all' | string[]            // default 'all'
  customValues?: SearchParamsValues
}

type SearchParamsValue =
  | string
  | number
  | boolean
  | null
  | SearchParamsValue[]
  | { [key: string]: SearchParamsValue }

type SearchParamsValues = Record<string, SearchParamsValue>

License

MIT