UNPKG

nuqs-svelte

Version:

Svelte adaptation of the `nuqs` library for managing URL query strings as state.

129 lines (128 loc) 5.19 kB
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, }; }