nuqs-svelte
Version:
Svelte adaptation of the `nuqs` library for managing URL query strings as state.
129 lines (128 loc) • 5.19 kB
JavaScript
import { useAdapter } from "./adapters/index.svelte";
import { debug } from "./debug";
import { emitter } from "./sync";
import { enqueueQueryStringUpdate, FLUSH_RATE_LIMIT_MS, getQueuedValue, scheduleFlushToURL, } from "./update-queue";
import { safeParse } from "./utils";
/**
* Svelte state hook synchronized with a URL query string in SvelteKit
*
* If used without a `defaultValue` supplied in the options, and the query is
* missing in the URL, the state will be `null`.
*
* ### Behaviour with default values:
*
* _Note: the URL will **not** be updated with the default value if the query
* is missing._
*
* Setting the value to `null` will clear the query in the URL, and return
* the default value as state.
*
* Example usage:
* ```svelte
* <script lang="ts">
* // Blog posts filtering by tag
* const tag = useQueryState('tag')
* const filteredPosts = posts.filter(post => tag ? post.tag === tag.current : true)
* const clearTag = () => tag.current = null
*
* // With default values
*
* const count = useQueryState(
* 'count',
* parseAsInteger.defaultValue(0)
* )
*
* const increment = () => count.current = (count.current ?? 0) + 1
* const decrement = () => count.current = (count.current ?? 0) - 1
* const clearCountQuery = () => count.current = null
*
* // --
*
* const date = useQueryState(
* 'date',
* parseAsIsoDateTime.withDefault(new Date('2021-01-01'))
* )
*
* const setToNow = () => date.current = new Date();
* const addOneHour = () => {
* date.current = new Date(date.current.valueOf() + 3600_000);
* }
* </script>
* ```
* @param key The URL query string key to bind to
* @param options - Parser (defines the state data type), optional default value and history mode.
*/
export function useQueryState(key, { history, shallow, scroll, throttleMs, parse = (x) => x, serialize = String, eq = (a, b) => a === b, defaultValue = undefined, clearOnDefault = true, } = {
parse: (x) => x,
serialize: String,
eq: (a, b) => a === b,
clearOnDefault: true,
defaultValue: undefined,
}) {
const adapter = useAdapter();
const initialSearchParams = $derived(adapter.searchParams());
let internalState = $state((() => {
const queuedQuery = getQueuedValue(key);
const query = queuedQuery === undefined ? (initialSearchParams.get(key) ?? null) : queuedQuery;
return query === null ? null : safeParse(parse, query, key);
})());
// this state should represent the previous query string value
// so that we can compare it with the current value
let queryState = initialSearchParams.get(key) ?? null;
$effect(() => {
const query = initialSearchParams.get(key) ?? null;
// parse the query string before comparing, as these values can be boolean, and any string would return true
const state = query === null ? null : safeParse(parse, query, key);
if (state === queryState) {
debug("[nuqs `%s`] syncFromUseSearchParams, no change, prev: %O, new: %O", key, queryState, state);
return;
}
debug("[nuqs `%s`] syncFromUseSearchParams %O", key, state);
internalState = state;
queryState = query;
});
$effect(() => {
const updateInternalState = ({ state, query }) => {
debug("[nuqs `%s`] updateInternalState %O", key, state);
internalState = state;
queryState = query;
};
debug("[nuqs `%s`] subscribing to sync", key);
emitter.on(key, updateInternalState);
return () => {
debug("[nuqs `%s`] unsubscribing from sync", key);
emitter.off(key, updateInternalState);
};
});
const update = (updater, options = {}) => {
let newValue = typeof updater === "function"
? updater((internalState ?? defaultValue ?? null))
: updater;
if ((options.clearOnDefault ?? clearOnDefault) &&
newValue !== null &&
defaultValue !== undefined &&
eq(newValue, defaultValue)) {
debug("[nuqs `%s`] clearing query string because the value is equal to the default value", key);
newValue = null;
}
const adapterOptions = adapter.options;
const query = enqueueQueryStringUpdate(key, newValue, serialize, {
// Call-level options take precedence over hook declaration options that take precedence over adapter-level options
history: options.history ?? history ?? adapterOptions?.history ?? "replace",
shallow: options.shallow ?? shallow ?? adapterOptions?.shallow ?? true,
scroll: options.scroll ?? scroll ?? adapterOptions?.scroll ?? false,
throttleMs: options.throttleMs ?? throttleMs ?? adapterOptions?.throttleMs ?? FLUSH_RATE_LIMIT_MS,
});
emitter.emit(key, { state: newValue, query });
return scheduleFlushToURL(adapter);
};
return {
get current() {
return (internalState ?? defaultValue ?? null);
},
set current(newValue) {
update(newValue);
},
set: update,
};
}