{"id":31941732,"url":"https://github.com/vp-tw/nanostores-qs","last_synced_at":"2026-04-11T11:05:22.992Z","repository":{"id":280255709,"uuid":"940997793","full_name":"vp-tw/nanostores-qs","owner":"vp-tw","description":"A reactive querystring manager using nanostores","archived":false,"fork":false,"pushed_at":"2025-10-01T02:21:46.000Z","size":1736,"stargazers_count":13,"open_issues_count":1,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-10-04T11:50:05.292Z","etag":null,"topics":["nanostores","persistent","qs","querystring","search","searchparams","typescript"],"latest_commit_sha":null,"homepage":"http://vdustr.dev/nanostores-qs/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/vp-tw.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-03-01T08:28:34.000Z","updated_at":"2025-10-01T13:17:03.000Z","dependencies_parsed_at":"2025-03-02T10:27:17.088Z","dependency_job_id":"6925b7ac-ee72-4262-baca-17280e414e30","html_url":"https://github.com/vp-tw/nanostores-qs","commit_stats":null,"previous_names":["vdustr/nanostore-qs","vp-tw/nanostores-qs","vdustr/nanostores-qs"],"tags_count":5,"template":false,"template_full_name":"VdustR/template-aio","purl":"pkg:github/vp-tw/nanostores-qs","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vp-tw%2Fnanostores-qs","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vp-tw%2Fnanostores-qs/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vp-tw%2Fnanostores-qs/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vp-tw%2Fnanostores-qs/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/vp-tw","download_url":"https://codeload.github.com/vp-tw/nanostores-qs/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vp-tw%2Fnanostores-qs/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279018499,"owners_count":26086383,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-14T02:00:06.444Z","response_time":60,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["nanostores","persistent","qs","querystring","search","searchparams","typescript"],"created_at":"2025-10-14T09:18:14.745Z","updated_at":"2025-10-14T09:18:16.264Z","avatar_url":"https://github.com/vp-tw.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# @vp-tw/nanostores-qs\n\nReactive, type-safe query string management built on top of [nanostores](https://github.com/nanostores/nanostores).\n\n## Why `@vp-tw/nanostores-qs`?\n\n- 🔄 Reactive stores that stay in sync with the URL.\n- 🔍 Type-safe parameter definitions with encode/decode.\n- 🧪 Dry-run URL generation via `.dry` (no history side effects) — great for link building and router integrations.\n- 🧩 Works with native `URLSearchParams` or custom libs like [`qs`](https://www.npmjs.com/package/qs) or [`query-string`](https://www.npmjs.com/package/query-string).\n- 🪝 Framework-friendly via Nanostores.\n- 🔢 Arrays, numbers, dates, and custom types.\n- ✅ Validation-friendly (zod, arktype, etc.).\n\n## Installation\n\n```bash\n# npm\nnpm install @vp-tw/nanostores-qs @nanostores/react nanostores\n\n# yarn\nyarn add @vp-tw/nanostores-qs @nanostores/react nanostores\n\n# pnpm\npnpm install @vp-tw/nanostores-qs @nanostores/react nanostores\n```\n\n## Quick Start\n\n```tsx\nimport { useStore } from \"@nanostores/react\";\nimport { createQsUtils } from \"@vp-tw/nanostores-qs\";\n\nconst qsUtils = createQsUtils();\nconst str = qsUtils.createSearchParamStore(\"str\");\n\nfunction StrInput() {\n  const value = useStore(str.$value); // string | undefined\n  return (\n    \u003cinput value={value ?? \"\"} onChange={(e) =\u003e str.update(e.target.value)} /\u003e\n  );\n}\n```\n\n## Core Concepts\n\n- `createQsUtils(options?)`: factory that exposes reactive URL state and helpers.\n  - `$search`: current `window.location.search` string.\n  - `$urlSearchParams`: `URLSearchParams` derived from `$search`.\n  - `$qs`: parsed query object (`string | string[] | undefined` values by default).\n  - `createSearchParamStore(name, config?)`: single-parameter store.\n  - `createSearchParamsStore(configs)`: multi-parameter store.\n  - `defineSearchParam(config).setEncode(fn)`: helper to attach an `encode` function.\n\n## Single-Parameter Store (`createSearchParamStore`)\n\nCreate a store for one query parameter. Configure decode/encode and defaults; update mutates history, and `.dry` returns the next search string without side effects.\n\n```tsx\n// num: number | \"\" (empty string) — demonstrates custom decode with defaultValue\nconst num = qsUtils.createSearchParamStore(\"num\", (def) =\u003e\n  def({ decode: (v) =\u003e (!v ? \"\" : Number(v)), defaultValue: \"\" }),\n);\n\n// Read in React\nconst value = useStore(num.$value);\n\n// Mutate URL\nnum.update(42); // push history\nnum.update(42, { replace: true, keepHash: true });\n\n// Dry-run: just compute next search\nconst nextSearch = num.update.dry(100); // \"?num=100\"\n```\n\nNotes:\n\n- When a new value equals the default, the parameter is removed from the URL.\n- Use `force: true` to bypass equality checks and always write.\n\n## Multi-Parameter Store (`createSearchParamsStore`)\n\nManage multiple query parameters together with ergonomic `update` and `updateAll`. Both have `.dry` counterparts for computing the next search string.\n\n```tsx\nconst filters = qsUtils.createSearchParamsStore((def) =\u003e ({\n  search: def({ defaultValue: \"\" }),\n  category: def({ isArray: true }),\n  minPrice: def({ decode: Number }).setEncode(String),\n  maxPrice: def({ decode: Number }).setEncode(String),\n  sortBy: def({ defaultValue: \"newest\" }),\n}));\n\n// Mutate URL\nfilters.update(\"minPrice\", 100);\nfilters.updateAll({\n  search: \"headphones\",\n  category: [\"wireless\", \"anc\"],\n  minPrice: 100,\n  maxPrice: 300,\n  sortBy: \"newest\",\n});\n\n// Dry-run (for links/router)\nconst preview = filters.updateAll.dry({\n  ...filters.$values.get(),\n  sortBy: \"price_asc\",\n});\n// \"?search=headphones\u0026category=wireless\u0026category=anc\u0026minPrice=100\u0026maxPrice=300\u0026sortBy=price_asc\"\n```\n\n## Router Integration\n\nUse `.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.\n\n```tsx\nimport { Link, useLocation, useNavigate } from \"react-router-dom\";\n\nconst location = useLocation();\n\nconst nextSearch = filters.updateAll.dry({\n  ...filters.$values.get(),\n  sortBy: \"price_desc\",\n});\n\n// 1) Plain anchor href\n\u003ca href={`${location.pathname}${nextSearch}`}\u003eApply filters\u003c/a\u003e;\n\n// 2) React Router \u003cLink\u003e\n\u003cLink\n  to={{\n    pathname: location.pathname,\n    search: nextSearch,\n  }}\n\u003e\n  Newest\n\u003c/Link\u003e;\n\n// 3) React Router navigate()\nconst navigate = useNavigate();\nfunction onApply() {\n  navigate({ pathname: location.pathname, search: nextSearch });\n}\n```\n\nNotes:\n\n- Calling `update`/`updateAll` mutates history directly and may bypass router-level hooks/blockers.\n- Prefer `.dry` + router navigation when your app relies on router features such as navigation blocking.\n\n## Good Practices\n\n- Integrate with routers using `.dry`:\n\n  - Generate `search` via `.dry` and hand it to your router (`Link`, `navigate`, etc.).\n  - Preserves router features like navigation blocking, transitions, scroll restoration, analytics, loaders.\n  - Avoids potential conflicts from calling the History API directly.\n\n- Update correlated params together with `createSearchParamsStore`:\n  - Example: when `search` changes, reset `page` to `1` in a single update to keep state consistent and produce a single history entry.\n\n```tsx\nconst qsUtils = createQsUtils();\n\n// Correlated params: search + page\nconst list = qsUtils.createSearchParamsStore((def) =\u003e ({\n  search: def({ defaultValue: \"\" }),\n  page: def({ decode: Number, defaultValue: 1 }).setEncode(String),\n}));\n\n// Good: one atomic update (single history entry, consistent UI)\nfunction onSearchChange(term: string) {\n  list.updateAll({ ...list.$values.get(), search: term, page: 1 });\n}\n\n// Bad: two separate single-param updates (can create two entries and transient states)\nconst searchStore = qsUtils.createSearchParamStore(\"search\", {\n  defaultValue: \"\",\n});\nconst pageStore = qsUtils.createSearchParamStore(\"page\", (def) =\u003e\n  def({ decode: Number, defaultValue: 1 }).setEncode(String),\n);\n\nfunction onSearchChangeBad(term: string) {\n  searchStore.update(term); // 1st history mutation\n  pageStore.update(1); // 2nd history mutation, possible transient UI state\n}\n```\n\n### Update Options\n\n`nanostores-qs` only mutates the parameter(s) it manages. Options:\n\n- `replace`: use `history.replaceState` instead of `pushState`.\n- `keepHash`: keep the current `location.hash` in the URL.\n- `state`: custom state passed to the History API.\n- `force`: bypass equality check and force an update.\n\n## Defaults and Equality\n\nIf a value equals its `defaultValue`, the parameter is removed from the URL to keep it clean. Customize equality with `isEqual` when creating the utils:\n\n```ts\nconst qsUtils = createQsUtils({\n  isEqual: (a, b) =\u003e JSON.stringify(a) === JSON.stringify(b),\n});\n```\n\nDefault `isEqual` comes from `es-toolkit`.\n\n## Validation and Custom Types\n\nYou can validate via `decode` and fall back to `defaultValue` on failure.\n\n```tsx\nimport { z } from \"zod\";\n\nconst SortOptionSchema = z.enum([\"newest\", \"price_asc\", \"price_desc\"]);\ntype SortOption = z.infer\u003ctypeof SortOptionSchema\u003e;\n\nconst sort = qsUtils.createSearchParamStore(\"sort\", {\n  decode: (v) =\u003e SortOptionSchema.parse(v),\n  defaultValue: SortOptionSchema[0],\n});\n\nfunction SortSelector() {\n  const option = useStore(sort.$value); // SortOption\n  return (\n    \u003cselect\n      value={option}\n      onChange={(e) =\u003e sort.update(e.target.value as SortOption)}\n    \u003e\n      {SortOptionSchema.options.map((o) =\u003e (\n        \u003coption key={o} value={o}\u003e\n          {o}\n        \u003c/option\u003e\n      ))}\n    \u003c/select\u003e\n  );\n}\n```\n\n## Using a Custom Query String Library\n\n```ts\nimport { parse, stringify } from \"qs\";\n\nconst qsUtils = createQsUtils({\n  qs: {\n    parse: (search) =\u003e parse(search, { ignoreQueryPrefix: true }),\n    stringify: (values) =\u003e stringify(values),\n  },\n});\n```\n\n## Routing Notes\n\n- When `window` is unavailable, the internal search defaults to an empty string; listeners are not attached.\n- The utils listen to `popstate` and patch `pushState/replaceState` to stay reactive with navigation.\n\n## Release\n\n```bash\npnpm pub\n```\n\n## License\n\n[MIT](./LICENSE)\n\nCopyright (c) 2025 ViPro \u003cvdustr@gmail.com\u003e (\u003chttp://vdustr.dev\u003e)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvp-tw%2Fnanostores-qs","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fvp-tw%2Fnanostores-qs","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvp-tw%2Fnanostores-qs/lists"}