On a current project, we wanted to hoist page state out of a useReducer hook into a query string of the URL. We saw this as a continual need for the project going forward, so we wanted a convenient way to build new pages that stored their state in the URL’s query string. To do so, we wrote a couple of React hooks to help us with the job.
The useReducer hook implements the Redux model for managing an individual component’s state. Given an initial state and a reducer function, which produces a new state given a previous state and an “action” which has occurred, useReducer
produces the current logical state and a dispatch
function which can be used to signal when an action has occurred.
We wanted a variant of useReducer
which stored its state not in a variable, but in the query string of the browser URL–something like:
const [state, dispatch] = useQueryStringReducer({
reducer,
initialState,
iso: stateToStringIso
});
The reducer and initial state are the same as you’d expect in a regular useReducer
or Redux use case. The iso
parameter is an isomorphism, a bidirectional conversion between the state type and query strings. Isomorphism
is simply a getter/setter pair with a fancy name:
export type Isomorphism<T, V> = {
to: (t: T) => V;
from: (v: V) => T;
};
We implemented useQueryStringReducer
in a few parts. First, we needed a way to get the current location
and history
object from react-router
. We pulled in a useRouter
hook we found on GitHub:
import { useContext } from "react";
import { __RouterContext } from "react-router";
export default function useRouter() {
return useContext(__RouterContext);
}
Next we wanted a userQueryString
hook that worked just like useState
, but saved the state in the location:
import { Isomorphism } from "@atomic-object/lenses";
import * as React from "react";
import useRouter from "./use-router";
export function useQueryString(opts: {
initialState: T;
iso: Isomorphism<T, string>;
}): [T, (v: T) => void] {
const router = useRouter();
const { location, history } = router;
const { initialState, iso } = opts;
const [desiredState, setDesiredState] = React.useState(() =>
location.search ? iso.from(location.search.slice(1)) : initialState
);
React.useEffect(() => {
const handler = setTimeout(
() => history.replace(`${location.pathname}?${iso.to(desiredState)}`),
10
);
return () => clearTimeout(handler);
}, [desiredState, iso, history, location.pathname]);
return [desiredState, setDesiredState];
}
useQueryString
is just a thin wrapper around useState
that initializes the state from the query string and updates the query string when the state variable changes.
With that, it’s a simple matter to hoist useQueryString
into a useReducer
-like API:
export function useQueryStringReducer(opts: {
reducer: React.Reducer<T, A>;
initialState: T;
iso: Isomorphism<T, string>;
}): [T, React.Dispatch] {
const { reducer, initialState, iso } = opts;
const [state, setState] = useQueryString({
initialState,
iso,
});
const dispatch = React.useCallback(
(action: A) => setState(reducer(state, action)),
[reducer, state]
);
return [state, dispatch];
}
And that’s it! We used the query-string package to implement an isomorphism between our state type and query strings, and we were able to replace our state management from this:
const [state, dispatch] = React.useReducer(reducer, initialState);
To this:
const [state, dispatch] = useQueryStringReducer({
reducer,
initialState,
iso: pageOptionsQueryStringIso,
});
Is there any playground for this hook? or something like git repo?