Skip to content

Instantly share code, notes, and snippets.

@danielberndt
Last active May 3, 2025 07:21
Show Gist options
  • Save danielberndt/998e69f236ff90ba988960bfce218c6d to your computer and use it in GitHub Desktop.
Save danielberndt/998e69f236ff90ba988960bfce218c6d to your computer and use it in GitHub Desktop.
Use Query State Hook
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