UNPKG

nuqs-svelte

Version:

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

134 lines (133 loc) 5.25 kB
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, }; }