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

@trydig/next-searchparams

v0.1.0

Published

Drop-in replacement for Next.js useSearchParams that returns a mutable URLSearchParams.

Readme

@trydig/next-searchparams

"use client";
import { useSearchParams } from "@trydig/next-searchparams";

function Filter() {
  const params = useSearchParams();
  return <input onChange={(e) => params.set("filter", e.target.value)} />;
}

Drop-in replacement for Next.js useSearchParams that returns a mutable URLSearchParams. Mutations (set / delete / append) batch into a single URL update via microtask.

  • Next.js ≥ 15, React ≥ 19, TypeScript ^6
  • Client-only ("use client")
  • Zero deps beyond next / react peers

Why not the built-in hook?

Next.js's useSearchParams returns a read-only URLSearchParams. To update the URL you do this dance:

"use client";
import { useRouter, usePathname, useSearchParams } from "next/navigation";

function Filter() {
  const router = useRouter();
  const pathname = usePathname();
  const params = useSearchParams();

  function setFilter(value: string) {
    const next = new URLSearchParams(params);
    next.set("filter", value);
    router.push(`${pathname}?${next.toString()}`);
  }
  // ...
}

Problems:

  1. Boilerplate. Clone → mutate → stringify → push. Every callsite.

  2. No batching. Two updates in same tick = two pushes = two history entries + two server roundtrips.

  3. Full server roundtrip on every change. router.push re-runs RSC even when only client state cares about param. Slow for tight interactions (typeaheads, sliders, tab state).

  4. Stale closures. Read params from React state, mutate, write — concurrent updates clobber each other.

  5. Bailout to client-side rendering. Next.js's useSearchParams forces entire route into CSR at build time unless every consumer wrapped in <Suspense>. Miss one boundary → whole page deopts from static to dynamic, build warns:

    useSearchParams() should be wrapped in a suspense boundary at page "/...".
    Read more: https://nextjs.org/docs/messages/missing-suspense-with-csr-bailout

    Why: built-in hook reads server-injected param state, so server must wait for client. This hook reads window.location.search via useSyncExternalStore with empty server snapshot — no server dependency, no bailout, no Suspense walls. Static routes stay static.

This hook fixes all four:

"use client";
import { useSearchParams } from "@trydig/next-searchparams";

function Filter() {
  const params = useSearchParams();
  return <input onChange={(e) => params.set("filter", e.target.value)} />;
}

params.set(...) queues; microtask flushes once with the merged result; URL updates via history.pushState (shallow, default) — no RSC roundtrip unless you opt in.

Install

bun add @trydig/next-searchparams
# or npm / pnpm / yarn

Usage

Read

Same shape as the built-in — URLSearchParams instance, re-renders on URL change:

const params = useSearchParams();
const q = params.get("q");

Mutate

params.set("page", "2");
params.append("tag", "react");
params.delete("filter");
params.delete("tag", "react"); // delete specific value

Multiple calls in same tick batch into one history entry:

params.set("page", "1");
params.set("sort", "asc");
params.delete("cursor");
// → single pushState, single re-render

Config

const params = useSearchParams({
  replace: false,  // pushState vs replaceState. default: false
  shallow: true,   // history API vs router.push (RSC refetch). default: true
  scroll: true,    // only applies when shallow=false. default: true
});

| Option | Default | Effect | | --------- | ------- | ------------------------------------------------------------------- | | replace | false | true → no new history entry | | shallow | true | truehistory.pushState only. falserouter.push (RSC) | | scroll | true | Passed to router when shallow: false |

Set shallow: false when server components depend on the params and need to re-render.

Patch history early (recommended)

history.pushState / replaceState don't fire events natively. This package patches them to dispatch a urlchange event so the hook can subscribe. The hook patches lazily on first subscribe, but third-party code that pushes state before the hook mounts will be missed.

Call patchHistory() from instrumentation-client.ts for earliest patch:

// instrumentation-client.ts
import { patchHistory } from "@trydig/next-searchparams/patch";
patchHistory();

Idempotent — safe to call multiple times.

How it works

  • useSyncExternalStore subscribes to popstate + custom urlchange. Snapshot = window.location.search.
  • Returns a Proxy<URLSearchParams> wrapping a fresh URLSearchParams(search). Intercepts set / delete / append, pushes into a queue, schedules flush() via queueMicrotask. Other methods pass through.
  • flush() reads window.location.search fresh (not the React snapshot — avoids stale state), applies queued actions, writes URL via history.pushState/replaceState (shallow) or router.push/replace (non-shallow). Re-flushes if queue grew during apply.
  • configRef read at flush time → latest props win.
  • Safari workaround: URLSearchParams.has(key, value) second arg unsupported in Safari → polyfilled via getAll().includes().

Caveats

  • Client-only. getServerSnapshot returns "" — SSR sees empty params. Read params on the server from the route's searchParams prop, not this hook.
  • Shallow updates skip RSC. With shallow: true (default), server components do not re-fetch. If a server component depends on the param, use shallow: false or read params from the route props server-side.
  • Mutation is async. params.set("x", "1"); params.get("x") on the next line returns the old value — the proxy wraps a snapshot. Read from the next render.

Lint rules

Both ship in this package — point your linter at the existing install, no extra deps.

ESLint (flat config)

// eslint.config.js
import trydig from "@trydig/next-searchparams/eslint-plugin";

export default [
  trydig.configs.recommended,
];

Or wire the rule manually:

import trydig from "@trydig/next-searchparams/eslint-plugin";

export default [
  {
    plugins: { "@trydig/next-searchparams": trydig },
    rules: {
      "@trydig/next-searchparams/prefer-trydig-search-params": "warn",
    },
  },
];

Autofix rewrites import { useSearchParams } from "next/navigation"import { useSearchParams } from "@trydig/next-searchparams". Other specifiers (useRouter, usePathname) stay on next/navigation.

Biome (GritQL plugin)

Biome ≥ 2.x. Reference the shipped plugin from biome.json:

{
  "plugins": [
    "./node_modules/@trydig/next-searchparams/biome/plugins/no-next-search-params.grit"
  ]
}

Emits a diagnostic on the offending import. No autofix — Biome plugin autofix not stable yet.

Development

bun install
bunx tsc --noEmit  # typecheck
bun test           # tests (none yet)

Two files, tightly coupled — see CLAUDE.md for invariants before changing flush logic.

License

UNLICENSED. INTERNAL TRYDIG USE ONLY.