'use client'; /** * useQueryState — typed `useState`-style hook backed by ONE URL query key. * * WHY: * `useQueryParams().get('page')` works, but you re-coerce to number * in every component. `useQueryState('page', parseAsInteger.withDefault(1))` * gives you `[number, setter]` directly, like `useState`. Setting to * the parser's default value clears the key from the URL (no `?page=1` * noise) — toggle off via `clearOnDefault: false`. * * Inspired by nuqs (47ng/nuqs) but framework-agnostic via our adapter. * * @example * const [page, setPage] = useQueryState('page', parseAsInteger.withDefault(1)); * const [tab, setTab] = useQueryState('tab', parseAsStringEnum(['a','b']).withDefault('a')); * setPage((p) => p + 1); // functional updater * setPage(null); // clear the key */ import { useCallback, useMemo } from 'react'; import { useLocation } from './useLocation'; import { useNavigate } from './useNavigate'; import type { QueryParser } from './parsers'; export interface UseQueryStateOptions { /** Use `replaceState` instead of `pushState`. Default: false. */ replace?: boolean; /** Scroll to top after navigation. Default: false. */ scroll?: boolean; /** * When the new value equals the parser's default, drop the key from * the URL instead of writing `?page=1`. Default: true (recommended — * keeps URLs clean). Set false if your URLs are linked / bookmarked * and you need explicit values. */ clearOnDefault?: boolean; } export type QueryStateUpdater = T | null | ((current: T) => T | null); // Two overloads so the return type narrows on `defaultValue`. export function useQueryState( key: string, parser: QueryParser & { defaultValue: T }, options?: UseQueryStateOptions ): [T, (next: QueryStateUpdater, opts?: UseQueryStateOptions) => void]; export function useQueryState( key: string, parser: QueryParser, options?: UseQueryStateOptions ): [T | null, (next: QueryStateUpdater, opts?: UseQueryStateOptions) => void]; export function useQueryState( key: string, parser: QueryParser, options?: UseQueryStateOptions ): [T | null, (next: QueryStateUpdater, opts?: UseQueryStateOptions) => void] { const { pathname, search } = useLocation(); const { navigate } = useNavigate(); const value = useMemo(() => { const raw = new URLSearchParams(search).get(key); if (raw === null) return parser.defaultValue ?? null; const parsed = parser.parse(raw); return parsed === null ? (parser.defaultValue ?? null) : parsed; }, [search, key, parser]); const setValue = useCallback( (next: QueryStateUpdater, callOpts?: UseQueryStateOptions) => { const resolved = typeof next === 'function' ? (next as (current: T) => T | null)( (value ?? parser.defaultValue) as T ) : next; const params = new URLSearchParams(search); const clearOnDefault = callOpts?.clearOnDefault ?? options?.clearOnDefault ?? true; if ( resolved === null || (clearOnDefault && parser.defaultValue !== undefined && parser.eq(resolved as T, parser.defaultValue)) ) { params.delete(key); } else { params.set(key, parser.serialize(resolved as T)); } const qs = params.toString(); const href = qs ? `${pathname}?${qs}` : pathname; navigate(href, { replace: callOpts?.replace ?? options?.replace ?? false, scroll: callOpts?.scroll ?? options?.scroll ?? false, }); }, [pathname, search, key, parser, navigate, options, value] ); return [value, setValue]; }