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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@vp-tw/nanostores-qs

v0.4.0

Published

A reactive querystring manager using nanostores

Downloads

3,082

Readme

@vp-tw/nanostores-qs

Reactive, type-safe query string management built on top of nanostores.

Why @vp-tw/nanostores-qs?

  • 🔄 Reactive stores that stay in sync with the URL.
  • 🔍 Type-safe parameter definitions with encode/decode.
  • 🧪 Dry-run URL generation via .dry (no history side effects) — great for link building and router integrations.
  • 🧩 Works with native URLSearchParams or custom libs like qs or query-string.
  • 🪝 Framework-friendly via Nanostores.
  • 🔢 Arrays, numbers, dates, and custom types.
  • ✅ Validation-friendly (zod, arktype, etc.).

Installation

# npm
npm install @vp-tw/nanostores-qs @nanostores/react nanostores

# yarn
yarn add @vp-tw/nanostores-qs @nanostores/react nanostores

# pnpm
pnpm install @vp-tw/nanostores-qs @nanostores/react nanostores

Quick Start

import { useStore } from "@nanostores/react";
import { createQsUtils } from "@vp-tw/nanostores-qs";

const qsUtils = createQsUtils();
const str = qsUtils.createSearchParamStore("str");

function StrInput() {
  const value = useStore(str.$value); // string | undefined
  return (
    <input value={value ?? ""} onChange={(e) => str.update(e.target.value)} />
  );
}

Core Concepts

  • createQsUtils(options?): factory that exposes reactive URL state and helpers.
    • $search: current window.location.search string.
    • $urlSearchParams: URLSearchParams derived from $search.
    • $qs: parsed query object (string | string[] | undefined values by default).
    • createSearchParamStore(name, config?): single-parameter store.
    • createSearchParamsStore(configs): multi-parameter store.
    • defineSearchParam(config).setEncode(fn): helper to attach an encode function.

Single-Parameter Store (createSearchParamStore)

Create a store for one query parameter. Configure decode/encode and defaults; update mutates history, and .dry returns the next search string without side effects.

// num: number | "" (empty string) — demonstrates custom decode with defaultValue
const num = qsUtils.createSearchParamStore("num", (def) =>
  def({ decode: (v) => (!v ? "" : Number(v)), defaultValue: "" }),
);

// Read in React
const value = useStore(num.$value);

// Mutate URL
num.update(42); // push history
num.update(42, { replace: true, keepHash: true });

// Dry-run: just compute next search
const nextSearch = num.update.dry(100); // "?num=100"

Notes:

  • When a new value equals the default, the parameter is removed from the URL.
  • Use force: true to bypass equality checks and always write.

Multi-Parameter Store (createSearchParamsStore)

Manage multiple query parameters together with ergonomic update and updateAll. Both have .dry counterparts for computing the next search string.

const filters = qsUtils.createSearchParamsStore((def) => ({
  search: def({ defaultValue: "" }),
  category: def({ isArray: true }),
  minPrice: def({ decode: Number }).setEncode(String),
  maxPrice: def({ decode: Number }).setEncode(String),
  sortBy: def({ defaultValue: "newest" }),
}));

// Mutate URL
filters.update("minPrice", 100);
filters.updateAll({
  search: "headphones",
  category: ["wireless", "anc"],
  minPrice: 100,
  maxPrice: 300,
  sortBy: "newest",
});

// Dry-run (for links/router)
const preview = filters.updateAll.dry({
  ...filters.$values.get(),
  sortBy: "price_asc",
});
// "?search=headphones&category=wireless&category=anc&minPrice=100&maxPrice=300&sortBy=price_asc"

Router Integration

Use .dry to generate the search string, then let your router perform navigation. This keeps router features (navigation blocking, data loaders, transitions, scroll restoration, analytics) intact and avoids conflicts with direct History API calls.

import { Link, useLocation, useNavigate } from "react-router-dom";

const location = useLocation();

const nextSearch = filters.updateAll.dry({
  ...filters.$values.get(),
  sortBy: "price_desc",
});

// 1) Plain anchor href
<a href={`${location.pathname}${nextSearch}`}>Apply filters</a>;

// 2) React Router <Link>
<Link
  to={{
    pathname: location.pathname,
    search: nextSearch,
  }}
>
  Newest
</Link>;

// 3) React Router navigate()
const navigate = useNavigate();
function onApply() {
  navigate({ pathname: location.pathname, search: nextSearch });
}

Notes:

  • Calling update/updateAll mutates history directly and may bypass router-level hooks/blockers.
  • Prefer .dry + router navigation when your app relies on router features such as navigation blocking.

Good Practices

  • Integrate with routers using .dry:

    • Generate search via .dry and hand it to your router (Link, navigate, etc.).
    • Preserves router features like navigation blocking, transitions, scroll restoration, analytics, loaders.
    • Avoids potential conflicts from calling the History API directly.
  • Update correlated params together with createSearchParamsStore:

    • Example: when search changes, reset page to 1 in a single update to keep state consistent and produce a single history entry.
const qsUtils = createQsUtils();

// Correlated params: search + page
const list = qsUtils.createSearchParamsStore((def) => ({
  search: def({ defaultValue: "" }),
  page: def({ decode: Number, defaultValue: 1 }).setEncode(String),
}));

// Good: one atomic update (single history entry, consistent UI)
function onSearchChange(term: string) {
  list.updateAll({ ...list.$values.get(), search: term, page: 1 });
}

// Bad: two separate single-param updates (can create two entries and transient states)
const searchStore = qsUtils.createSearchParamStore("search", {
  defaultValue: "",
});
const pageStore = qsUtils.createSearchParamStore("page", (def) =>
  def({ decode: Number, defaultValue: 1 }).setEncode(String),
);

function onSearchChangeBad(term: string) {
  searchStore.update(term); // 1st history mutation
  pageStore.update(1); // 2nd history mutation, possible transient UI state
}

Update Options

nanostores-qs only mutates the parameter(s) it manages. Options:

  • replace: use history.replaceState instead of pushState.
  • keepHash: keep the current location.hash in the URL.
  • state: custom state passed to the History API.
  • force: bypass equality check and force an update.

Defaults and Equality

If a value equals its defaultValue, the parameter is removed from the URL to keep it clean. Customize equality with isEqual when creating the utils:

const qsUtils = createQsUtils({
  isEqual: (a, b) => JSON.stringify(a) === JSON.stringify(b),
});

Default isEqual comes from es-toolkit.

Validation and Custom Types

You can validate via decode and fall back to defaultValue on failure.

import { z } from "zod";

const SortOptionSchema = z.enum(["newest", "price_asc", "price_desc"]);
type SortOption = z.infer<typeof SortOptionSchema>;

const sort = qsUtils.createSearchParamStore("sort", {
  decode: (v) => SortOptionSchema.parse(v),
  defaultValue: SortOptionSchema[0],
});

function SortSelector() {
  const option = useStore(sort.$value); // SortOption
  return (
    <select
      value={option}
      onChange={(e) => sort.update(e.target.value as SortOption)}
    >
      {SortOptionSchema.options.map((o) => (
        <option key={o} value={o}>
          {o}
        </option>
      ))}
    </select>
  );
}

Using a Custom Query String Library

import { parse, stringify } from "qs";

const qsUtils = createQsUtils({
  qs: {
    parse: (search) => parse(search, { ignoreQueryPrefix: true }),
    stringify: (values) => stringify(values),
  },
});

Routing Notes

  • When window is unavailable, the internal search defaults to an empty string; listeners are not attached.
  • The utils listen to popstate and patch pushState/replaceState to stay reactive with navigation.

Release

pnpm pub

License

MIT

Copyright (c) 2025 ViPro [email protected] (http://vdustr.dev)