A Simple useUrlParams Hook
Maintaining web application state in URLs has some benefits: it makes state "visible" to users, allows to easily share state, and avoids conflicts with other mechanisms like localStorage
when using multiple tabs.
Relying on URL query parameters to maintain state in React may not be as straight-forward as one thinks, though. The answers to this very popular StackOverflow question fall into one of two camps:
- Relying on React Router, which can be a heavy-handed approach if one does not a full-fledged router.
- Using the browser's
URLSearchParams
interface, which only retrieves query params once, though, while it might be desired to re-render components if the URL is changed (viaHistory.pushState()
orHistory.replaceState()
).
Fortunately, React 18's built-in useSyncExternalStore
hook makes it possible to build a small, reactive useUrlParams
hook. This is the TypeScript code:
1import { useCallback, useMemo, useSyncExternalStore } from 'react';2type PushStateParameters = Parameters<typeof window.history.pushState>;3type Listener = () => void;45let urlParams = new URLSearchParams(window.location.search);67window.history.pushState = new Proxy(window.history.pushState, {8apply: (fn, thisArg, pushStateArgs) => {9urlParams = new URLSearchParams(pushStateArgs[2]);10listeners.forEach((listener) => listener());11return fn.apply(thisArg, pushStateArgs as PushStateParameters);12},13});1415const listeners = new Set<Listener>();1617function subscribe(listener: Listener) {18listeners.add(listener);19return () => listeners.delete(listener);20}2122export function useUrlParams() {23return useSyncExternalStore(24subscribe,25useCallback(() => urlParams, [urlParams])26);27}2829export function useUrlParam(name: string) {30return useSyncExternalStore(31subscribe,32useCallback(() => urlParams.get(name), [urlParams.get(name), name])33);34}
After imports and type definitions, line 5
defines the initial state relying on the URLSearchParams
interface. Lines 7
to 13
setup a Proxy
object, which intercepts any calls to pushState
to update urlParams
and then informs all subscribed listeners to get an updated state snapshot (details in the useSyncExternalStore
docs). Alternatively / in addition, the proxy could also be set up for replaceState
.
Lines 15
through 20
define a Set
holding listeners that subscribe to state updates, and a basic subscribe
function for adding listeners to said set and removing them once they unmount.
Relying on this setup, two hooks can be defined:
useUrlParams
returns the latestUrlSearchParams
object instance. Components using this hook re-render whenever any query parameter is added / changed / removed viapushState
.useUrlParam
takes as a single input thename
of a query parameter, and returns its string value (ornull
, if it does not exist in the URL). Components using this hook re-render only when a query parameter with that given name is added / changed / removed viapushState
.