nuqs-svelte
Version:
Svelte adaptation of the `nuqs` library for managing URL query strings as state.
134 lines (133 loc) • 5.25 kB
JavaScript
import { untrack } from "svelte";
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 = "replace", shallow = true, scroll = false, throttleMs = FLUSH_RATE_LIMIT_MS, parse = (x) => x, serialize = String, eq = (a, b) => a === b, defaultValue = undefined, clearOnDefault = true, } = {
history: "replace",
scroll: false,
shallow: true,
throttleMs: 50,
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);
})());
let queryState = $derived(initialSearchParams.get(key) ?? null);
$effect(() => {
// the query state needs to be untracked, as its value is updated when the internal state changes,
// and causes this effect to re-run, causing the internal state to be set to it's older value
const uQueryState = untrack(() => queryState);
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 === uQueryState) {
debug("[nuqs `%s`] syncFromUseSearchParams, no change, prev: %O, new: %O", key, uQueryState, 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 query = enqueueQueryStringUpdate(key, newValue, serialize, {
// Call-level options take precedence over hook declaration options
history: options.history ?? history,
shallow: options.shallow ?? shallow,
scroll: options.scroll ?? scroll,
throttleMs: options.throttleMs ?? throttleMs,
});
emitter.emit(key, { state: newValue, query });
return scheduleFlushToURL(adapter);
};
return {
get current() {
return (internalState ?? defaultValue ?? null);
},
set current(newValue) {
update(newValue);
},
set: update,
};
}