@query-state/core
v1.0.3
Published
Core query state management
Maintainers
Readme
@query-state/core
@query-state/core is the core package of the Query State library, a lightweight and reactive query manager for URL state.
It leverages observer patterns and a pub/sub system to allow fine-grained control over query parameters in your application.
📦 Installation
npm i @query-state/core
# or
pnpm i @query-state/core
💡 Overview
This package provides a Query Manager that:
Observes the browser URL and query parameters
Notifies subscribers only for the keys that changed
Applies updates in order using a microtask queue to avoid race conditions
Patches history.pushState and history.replaceState for controlled updates
Supports silent updates without triggering notifications
Handles rate-limiting for high-frequency updates
⚙️ How the Query Manager Works
The core of @query-state/core is the Query Manager, implemented via the ImpQueryManager class.
It:
- Listens to changes in the URL via:
popstate events (browser back/forward)
Custom qschange events dispatched on query updates
Maintains a subscriber list per query key for granular notifications
Uses a microtask queue to apply multiple updates in order
Rate-limits updates with configurable options
Patches the browser history API (pushState / replaceState)
Allows silent updates that modify the URL without notifying subscribers
Internal Flow
sequenceDiagram participant Browser as Browser URL participant Manager as QueryManager participant Queue as QueryQueue participant Consumer as Subscriber
Browser->>Manager: URL changes (popstate or __qschange__)
Manager->>Queue: Enqueue updates in microtask
Queue->>Manager: Apply queued updates
Manager->>Consumer: Notify subscribers of changed keysKey points:
Granular Updates: Only affected subscribers are notified.
Microtask Queue: Ensures updates are applied in order.
Silent Updates: Modify URL without notifications (silentUpdate).
📦 Public API
@query-state/core exposes convenient accessors for interacting with the query state:
| Function | Description |
| --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| readParam(key: string) | Reads the current value of a query key from the URL. |
| writeParam(key: string, value: string \| null, mode?: 'push' \| 'replace', options?: FullOptions) | Updates a query key in the URL and notifies subscribers. |
| subscribeQS(callback: () => void, key: string) | Subscribes to changes of a specific query key. Returns an unsubscribe function. |
Example: Subscribing and Updating Query Keys
import { subscribeQS, writeParam, readParam } from '@query-state/core';
// Subscribe to a query key
const unsubscribePage = subscribeQS(() => {
const page = readParam('page');
console.log('Page changed:', page);
}, 'page');
// Update a query key
writeParam('page', '2'); // triggers __qschange__ event
// Later, if you want to stop listening
unsubscribePage();Example: Using Accessors for Multiple Keys
import { readParam, writeParam, subscribeQS } from '@query-state/core';
// Read a key
console.log(readParam('limit')); // e.g. "10"
// Subscribe to changes
const unsubscribeLimit = subscribeQS(() => {
console.log('Limit changed:', readParam('limit'));
}, 'limit');
// Update a key
writeParam('limit', '20'); // triggers subscriber
// Stop listening when done
unsubscribeLimit();📖 Features
Reactive URL state: Automatically observes changes in query parameters.
Granular updates: Only notifies subscribers of keys that changed.
Safe history API patching: Avoids breaking browser back/forward navigation.
Queue system: Applies multiple updates in order to avoid conflicts.
Rate-limiting support: Prevents excessive updates in high-frequency scenarios.
Silent updates: Modify URL without triggering subscribers.
TypeScript support: Full typings for queries, options, and events.
🔹 Query State Parsers (qsParser)
@query-state/core provides a set of type-safe parsers to convert query string values from the URL into strongly-typed data and back.
These parsers are built using the createParser builder, allowing customization, default values, and equality checks.
⚙️ How it works
Each parser implements a parse/serialize interface:
interface ParserConfig<T> {
parse?: (value: string) => T | null;
serialize?: (value: T) => string;
equals?: (a: T, b: T) => boolean;
}parse: Converts a string from the URL to a typed value.
serialize: Converts a typed value back to a string for the URL.
equals (optional): Compares two values for equality.
You can also extend parsers with options like defaultValue, history mode, rateLimit, and more.
🔹 Built-in Parsers
(Boolean):
import { qsParserBoolean } from '@query-state/core';
const parsed = qsParserBoolean.parse('true'); // true
const serialized = qsParserBoolean.serialize(false); // "false"(String):
import { qsParserString } from '@query-state/core';
qsParserString.parse('hello'); // "hello"
qsParserString.serialize('world'); // "world"(Integer & Float):
import { qsParserInteger, qsParserFloat } from '@query-state/core';
qsParserInteger.parse('42'); // 42
qsParserFloat.parse('3.14'); // 3.14(Date & Timestamp):
import {
qsParserTimestamp,
qsParserDateTime,
qsParserISODate,
} from '@query-state/core';
qsParserTimestamp.parse('1697685600000'); // Date object from milliseconds
qsParserDateTime.parse('2025-10-18T23:00:00.000Z'); // Date object from ISO string
qsParserISODate.parse('2025-10-18'); // Date at midnight UTC(Literal Types):
import {
qsParserStringLiteral,
qsParserNumberLiteral,
} from '@query-state/core';
const colorParser = qsParserStringLiteral(['red', 'green', 'blue']);
colorParser.parse('red'); // "red"
colorParser.parse('yellow'); // null
const numberParser = qsParserNumberLiteral([1, 2, 3]);
numberParser.parse('2'); // 2
numberParser.parse('5'); // null(JSON):
import { qsParserJson } from '@query-state/core';
const userParser = qsParserJson<{ id: number; name: string }>();
userParser.parse('{"id":1,"name":"Pedro"}'); // { id: 1, name: "Pedro" }
userParser.serialize({ id: 2, name: 'Alice' }); // '{"id":2,"name":"Alice"}'(Arrays):
import { qsParserArray, qsParserInteger } from '@query-state/core';
const arrayParser = qsParserArray(qsParserInteger, ',');
arrayParser.parse('1,2,3'); // [1, 2, 3]
arrayParser.serialize([4, 5, 6]); // "4,5,6"💡 Customization & Defaults
All parsers can be extended with defaults and custom options:
const booleanWithDefault = qsParserBoolean.setDefault(true);
const stringLimited = qsParserString.defineOptions({ history: 'push' });🔹 Use Case
When reading or writing query parameters, you can parse safely into the expected type and serialize back to the URL, ensuring type safety and avoiding invalid values:
import { readParam, writeParam } from '@query-state/core';
const page = qsParserInteger.parse(readParam('page')); // safely parse page number
writeParam('page', qsParserInteger.serialize(2)); // update URL🛠️ Advanced Usage
You can integrate @query-state/core with any frontend framework (React, Vue, etc.) to manage URL query state reactively, ensuring your UI is always in sync with the browser URL.
