React Hook for Query String State Management

On a current project, we wanted to hoist page state out of a [useReducer](https://reactjs.org/docs/hooks-reference.html#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](https://reactjs.org/docs/hooks-intro.html) to help us with the job.

The [useReducer](https://reactjs.org/docs/hooks-reference.html#usereducer) hook implements the [Redux](https://redux.js.org) 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](https://github.com/atomicobject/lenses#isomorphisms), a bidirectional conversion between the state type and query strings. `Isomorphism` is simply a getter/setter pair with a fancy name:


export type Isomorphism = {
	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](https://github.com/ReactTraining/react-router/issues/6430#issuecomment-434515664):


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, (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;
  initialState: T;
  iso: Isomorphism;
}): [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](https://www.npmjs.com/package/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,
});
Conversation
  • Szk says:

    Is there any playground for this hook? or something like git repo?

  • Comments are closed.