The personal website of @erikwittern

A Simple useUrlParams Hook

February 20th 2023

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:

  1. Relying on React Router, which can be a heavy-handed approach if one does not a full-fledged router.
  2. 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 (via History.pushState() or History.replaceState()).

Fortunately, React 18's built-in useSyncExternalStore hook makes it possible to build a small, reactive useUrlParams hook. This is the TypeScript code:

1
import { useCallback, useMemo, useSyncExternalStore } from 'react';
2
type PushStateParameters = Parameters<typeof window.history.pushState>;
3
type Listener = () => void;
4
5
let urlParams = new URLSearchParams(window.location.search);
6
7
window.history.pushState = new Proxy(window.history.pushState, {
8
apply: (fn, thisArg, pushStateArgs) => {
9
urlParams = new URLSearchParams(pushStateArgs[2]);
10
listeners.forEach((listener) => listener());
11
return fn.apply(thisArg, pushStateArgs as PushStateParameters);
12
},
13
});
14
15
const listeners = new Set<Listener>();
16
17
function subscribe(listener: Listener) {
18
listeners.add(listener);
19
return () => listeners.delete(listener);
20
}
21
22
export function useUrlParams() {
23
return useSyncExternalStore(
24
subscribe,
25
useCallback(() => urlParams, [urlParams])
26
);
27
}
28
29
export function useUrlParam(name: string) {
30
return useSyncExternalStore(
31
subscribe,
32
useCallback(() => 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:

  1. useUrlParams returns the latest UrlSearchParams object instance. Components using this hook re-render whenever any query parameter is added / changed / removed via pushState.
  2. useUrlParam takes as a single input the name of a query parameter, and returns its string value (or null, 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 via pushState.