Last active
May 3, 2025 07:21
-
-
Save danielberndt/998e69f236ff90ba988960bfce218c6d to your computer and use it in GitHub Desktop.
Use Query State Hook
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import type {ComponentProps, Dispatch, ReactElement} from "react"; | |
type LinkProps<T> = {to: T} & Omit<ComponentProps<"a">, "href">; | |
export type QSLink<T> = (props: LinkProps<T>) => ReactElement; | |
export const buildLink = <T,>(setState: Dispatch<T>, getUrl: (t: T) => string): QSLink<T> => { | |
const Link = (props: LinkProps<T>) => { | |
const {onClick, to, ...rest} = props; | |
const handleClick: ComponentProps<"a">["onClick"] = (event) => { | |
if (event.ctrlKey || event.metaKey || event.altKey || event.shiftKey || event.button !== 0) | |
return; | |
onClick?.(event); | |
if (!event.defaultPrevented) { | |
event.preventDefault(); | |
setState(to); | |
} | |
}; | |
return <a href={getUrl(to)} onClick={handleClick} {...rest} />; | |
}; | |
return Link; | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {useRef, useState, useSyncExternalStore, type Dispatch} from "react"; | |
import {buildLink} from "./Link"; | |
import {shallow} from "./shallow"; | |
const eventPopstate = "popstate"; | |
const eventPushState = "pushState"; | |
const eventReplaceState = "replaceState"; | |
const eventHashchange = "hashchange"; | |
const events = [eventPopstate, eventPushState, eventReplaceState, eventHashchange]; | |
const subscribeToLocationUpdates = (callback: () => void) => { | |
for (const event of events) { | |
addEventListener(event, callback); | |
} | |
return () => { | |
for (const event of events) { | |
removeEventListener(event, callback); | |
} | |
}; | |
}; | |
const patchKey = Symbol.for("wouter_v3"); | |
// While History API does have `popstate` event, the only | |
// proper way to listen to changes via `push/replaceState` | |
// is to monkey-patch these methods. | |
// | |
// See https://stackoverflow.com/a/4585031 | |
if (typeof history !== "undefined" && typeof window[patchKey as any] === "undefined") { | |
for (const type of [eventPushState, eventReplaceState]) { | |
const original = (history as any)[type]; | |
(history as any)[type] = function () { | |
const result = original.apply(this, arguments); | |
const event = new Event(type) as any; | |
event.arguments = arguments; | |
dispatchEvent(event); | |
return result; | |
}; | |
} | |
Object.defineProperty(window, patchKey, {value: true}); | |
} | |
const parseKey = <T>(key: string, parse: (qs: string) => T) => { | |
const query = new URLSearchParams(window.location.search); | |
const value = query.get(key); | |
if (!value) return null; | |
return parse(value); | |
}; | |
const getUrl = (key: string, value: string | null) => { | |
if (!value) return ""; | |
const qs = new URLSearchParams(); | |
qs.set(key, value); | |
return `?${qs.toString()}`; | |
}; | |
export const useQueryState = <T>( | |
key: string, | |
options: { | |
defaultValue: T; | |
parse: (value: string) => T; | |
stringify: (value: T) => string; | |
} | |
) => { | |
const {defaultValue, parse, stringify} = options; | |
const lastRes = useRef<T | undefined>(undefined); | |
const value = useSyncExternalStore(subscribeToLocationUpdates, () => { | |
const next = parseKey(key, parse) ?? options.defaultValue; | |
if (shallow(lastRes.current, next)) return lastRes.current as T; | |
lastRes.current = next; | |
return next; | |
}); | |
const getUrlForValue = (value: T) => { | |
if (shallow(value, defaultValue)) return "."; | |
return getUrl(key, stringify(value)); | |
}; | |
const setValue: Dispatch<T> = (value) => { | |
window.history.pushState(null, "", getUrlForValue(value)); | |
}; | |
const [Link] = useState(() => buildLink(setValue, getUrlForValue)); | |
return [value, setValue, Link] as const; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment