Svelte adaptation of the `nuqs` library for managing URL query strings as state.
npm install nuqs-svelte


!dependabot
Type-safe search params state manager for Svelte. Like $state(), but stored in the URL query string.
- 🔀 new: Supports SvelteKit, and custom routers via adapters
- 🧘♀️ Simple: the URL is the source of truth
- 🕰 Replace history or append to use the Back button to navigate state updates
- ⚡️ Built-in parsers for common state types (integer, float, boolean, Date, and more). Create your own parsers for custom types & pretty URLs
- ♊️ Related querystrings with useQueryStates
- 📡 Shallow mode by default for URL query updates
``shell`
pnpm add nuqs-svelte
`shell`
yarn add nuqs-svelte
`shell`
npm install nuqs-svelte
You will need to wrap your Svelte component tree with an adapter. _(expand the appropriate section below)_
SvelteKit
> Supported Svelte versions: >=5.0.0.
`svelte filename="src/routes/+layout.svelte"
// src/routes/+layout.svelte
{@render children()}
`
`svelte filename="src/routes/+page.svelte"
// src/routes/+page.svelte
useQueryState takes one required argument: the key to use in the query string.It returns an object with the value present in the query
string as a string (or
null if none was found).Example outputs for our hello world example:
| URL | name value | Notes |
| ------------ | ---------- | ----------------------------------------------------------------- |
|
/ | null | No name key in URL |
| /?name= | '' | Empty string |
| /?name=foo | 'foo' |
| /?name=2 | '2' | Always returns a string by default, see Parsing below |Parsing
If your state type is not a string, you must pass a parsing function in the
second argument object.
We provide parsers for common and more advanced object types:
`ts
import {
parseAsString,
parseAsInteger,
parseAsFloat,
parseAsBoolean,
parseAsTimestamp,
parseAsIsoDateTime,
parseAsArrayOf,
parseAsJson,
parseAsStringEnum,
parseAsStringLiteral,
parseAsNumberLiteral,
} from "nuqs-svelte";useQueryState("tag"); // defaults to string
useQueryState("count", parseAsInteger);
useQueryState("brightness", parseAsFloat);
useQueryState("darkMode", parseAsBoolean);
useQueryState("after", parseAsTimestamp); // state is a Date
useQueryState("date", parseAsIsoDateTime); // state is a Date
useQueryState("array", parseAsArrayOf(parseAsInteger)); // state is number[]
useQueryState("json", parseAsJson()); // state is a Point
// Enums (string-based only)
enum Direction {
up = "UP",
down = "DOWN",
left = "LEFT",
right = "RIGHT",
}
const direction = useQueryState(
"direction",
parseAsStringEnum(Object.values(Direction)) // pass a list of allowed values
.withDefault(Direction.up),
);
// Literals (string-based only)
const colors = ["red", "green", "blue"] as const;
const color = useQueryState(
"color",
parseAsStringLiteral(colors) // pass a readonly list of allowed values
.withDefault("red"),
);
// Literals (number-based only)
const diceSides = [1, 2, 3, 4, 5, 6] as const;
const side = useQueryState(
"side",
parseAsNumberLiteral(diceSides) // pass a readonly list of allowed values
.withDefault(4),
);
`You may pass a custom set of
parse and serialize functions:`svelte
`> Note: parsers don't validate your data. If you expect positive integers
> or JSON-encoded objects of a particular shape, you'll need to feed the result
> of the parser to a schema validation library, like Zod.
Default value
When the query string is not present in the URL, the default behaviour is to
return
null as state.It can make state updating and UI rendering tedious. Take this example of a simple counter stored in the URL:
`svelte
count: {count.current}
`You can specify a default value to be returned in this case:
`ts
`Note: the default value is internal to Svelte, it will not be written to the
URL.
Setting the state to
null will remove the key in the query string and set the
state to the default value.Options
$3
By default, state updates are done by replacing the current history entry with
the updated query when state changes.
You can see this as a sort of
git squash, where all state-changing
operations are merged into a single history value.You can also opt-in to push a new history item for each state change,
per key, which will let you use the Back button to navigate state
updates:
`ts
// Default: replace current history with new state
useQueryState("foo", { history: "replace" });// Append state changes to history:
useQueryState("foo", { history: "push" });
`Any other value for the
history option will fallback to the default.You can also override the history mode when calling the state updater function:
`ts
const query = useQueryState("q", { history: "push" });// This overrides the hook declaration setting:
query.set(null, { history: "replace" });
`$3
> Note: this feature only applies to the SvelteKit adapter
By default, query state updates are done in a _client-first_ manner: there are
no network calls to the server.
To opt-in to query updates notifying the server (re-fetch data from loaders),
you can set
shallow to false:`ts
const state = useQueryState("foo", { shallow: false });// You can also pass the option on calls to setState:
state.set("bar", { shallow: false });
`$3
Because of browsers rate-limiting the History API, internal updates to the
URL are queued and throttled to a default of 50ms, which seems to satisfy
most browsers even when sending high-frequency query updates, like binding
to a text input or a slider.
Safari's rate limits are much higher and would require a throttle of around 340ms.
If you end up needing a longer time between updates, you can specify it in the
options:
`ts
useQueryState("foo", {
// Send updates to the server maximum once every second
shallow: false,
throttleMs: 1000,
});// You can also pass the option on calls to setState:
state.set("bar", { throttleMs: 1000 });
`> Note: the state returned by the hook is always updated instantly, to keep UI responsive.
> Only changes to the URL, and server requests when using
shallow: false, are throttled.If multiple hooks set different throttle values on the same event loop tick,
the highest value will be used. Also, values lower than 50ms will be ignored,
to avoid rate-limiting issues. Read more.
Configuring parsers, default value & options
You can use a builder pattern to facilitate specifying all of those things:
`ts
useQueryState(
"counter",
parseAsInteger.withDefault(0).withOptions({
history: "push",
shallow: false,
}),
);
`You can get this pattern for your custom parsers too, and compose them
with others:
`ts
import { createParser, parseAsHex } from "nuqs-svelte";// Wrapping your parser/serializer in
createParser
// gives it access to the builder pattern & server-side
// parsing capabilities:
const hexColorSchema = createParser({
parse(query) {
if (query.length !== 6) {
return null; // always return null for invalid inputs
}
return {
// When composing other parsers, they may return null too.
r: parseAsHex.parse(query.slice(0, 2)) ?? 0x00,
g: parseAsHex.parse(query.slice(2, 4)) ?? 0x00,
b: parseAsHex.parse(query.slice(4)) ?? 0x00,
};
},
serialize({ r, g, b }) {
return parseAsHex.serialize(r) + parseAsHex.serialize(g) + parseAsHex.serialize(b);
},
})
// Eg: set common options directly
.withOptions({ history: "push" });// Or on usage:
useQueryState(
"tribute",
hexColorSchema.withDefault({
r: 0x66,
g: 0x33,
b: 0x99,
}),
);
`Multiple Queries (batching)
You can call as many state update function as needed in a single event loop
tick, and they will be applied to the URL asynchronously:
`ts
`If you wish to know when the URL has been updated, and what it contains, you can
await the Promise returned by the state updater function, which gives you the
updated URLSearchParameters object:
`ts
const randomCoordinates = () => {
setLat(42);
return setLng(12);
};randomCoordinates().then((search: URLSearchParams) => {
search.get("lat"); // 42
search.get("lng"); // 12, has been queued and batch-updated
});
`
Implementation details (Promise caching)
The returned Promise is cached until the next flush to the URL occurs,
so all calls to a
state.set (of any hook) in the same event loop tick will
return the same Promise reference.Due to throttling of calls to the Web History API, the Promise may be cached
for several ticks. Batched updates will be merged and flushed once to the URL.
This means not every
state.set will reflect to the URL, if another one comes
overriding it before flush occurs.The returned Svelte state will reflect all set values instantly,
to keep UI responsive.
---
useQueryStatesFor query keys that should always move together, you can use
useQueryStates
with an object containing each key's type:`ts
import { useQueryStates, parseAsFloat } from "nuqs-svelte";const coordinates = useQueryStates(
{
lat: parseAsFloat.withDefault(45.18),
lng: parseAsFloat.withDefault(5.72),
},
{
history: "push",
},
);
const { lat, lng } = coordinates;
// Set all (or a subset of) the keys in one go:
const search = await coordinates.set({
lat: Math.random() * 180 - 90,
lng: Math.random() * 360 - 180,
});
`License