DEV Community

Cover image for Hooks to persist state in the query string or history.state in React
Felix Leupold
Felix Leupold

Posted on

Hooks to persist state in the query string or history.state in React

TLDR;

  • embrace the URL and put part of your state into the query string of the URL (?simple=true)
  • automatically support bookmarking, back, forward & refresh actions in the browser by doing so
  • useQueryState from use-location-state helps to put state into the query string
  • useQueryState(itemName, defaultValue) works like useState(), but persists the state in the query string

Intro: Benefits of good URLs

URLs are a fundamental technology of the web since their definition in the mid-90s. Being able to deep-link from one document to another was such a great idea that mobile operating systems even copied the principle for their app platforms. Apple calls them Universal Links, and Google calls them App Links. While native apps only surface a URL from time to time via the share sheet, URLs are always visible and accessible in the browser. So websites should maintain useful URLs.

Good URLs enable users to keep a reference to web applications in a specific state or to share them with other people. While the path of the URL usually defines the page the user is on, eg. a search results page (/search), the query string is often used to encode a custom state of that page, eg. a search query for "shoes" with a set of filters for colors and size (/search?q=shoes&color=blue&color=black&size=44). A user could now bookmark this URL to come back later or share it with a friend or click on one of the products to check it out in detail, and if they want to return to the results page, they could just use the back functionality to go back to the same search results and select another product.

Challenges: Maintaining the URL is hard... so we rarely do it

While the benefits of good URLs are apparent to most people, many modern SPAs build with frameworks like React still struggle to provide good URLs, because updating the URL and query string is harder than updating a local state or the redux store. I've been guilty of this myself, and I think the leading cause for this was the lack of an easy-to-use API.

An important design goal was to enable independent components on the page to take advantage of the query string and history.state, without them needing to know about each other. So a component only concerned about a specific part of the state, for example, the size filter parameter (?...&size=44), could read and update that state without having to deal with any other information stored in the query string.

Introducing: useQueryState()

I went ahead to create a simple, yet powerful hook for React that works like useState(), but persists state in the query string of the URL. All that you need to use it is to choose a parameter name and pass a default value. The API looks like this:

const [currentValue, updateValueFunction] = useQueryState(paramName, defaultValue)
Enter fullscreen mode Exit fullscreen mode

The default value will be returned as the current value, as long as the value was not updated and the query string does not include a value for that parameter yet. In case this syntax (array destructuring) is new to you, I recommend reading about it in the React Docs.

function Search() {
  const [queryString, setQueryString] = useQueryState("queryString", "");
  return (
    <label>
      What are you looking for?
      <input
        value={queryString}
        onChange={e => setQueryString(e.target.value)}
        placeholder="Shoes, Sunglasses, ..."
      />
    </label>
  );
}
Enter fullscreen mode Exit fullscreen mode

Demo | Code in CodeSandbox

When a user now types a search term "shoes" into the text field, the query string of the URL will be updated to /?queryString=shoes. And you can reload, or go another page and return and the state will be restored correctly.

Demo of use Query State

You can of course also use multiple useQueryState() hooks in a single component (or in separate components). Each useQueryState() automatically merges its updates with the currently encoded state in the query string.

const  [queryString, setQueryString] =  useQueryState("queryString",  "");
const  [colors, setColors] =  useQueryState("colors",  []);

const toggleColor = e => {
  const color = e.target.value;
  setColors(
    colors.includes(color)
      ? colors.filter(t => t !== color)
      : [...colors, color]
  );
};

return (
  <form>
    ...
    <Color
      name="red"
      active={colors.includes("red")}
      onChange={toggleColor}
    />
    <Color
      name="blue"
      active={colors.includes("blue")}
      onChange={toggleColor}
    />
    ...
  </form>
)
Enter fullscreen mode Exit fullscreen mode

Demo | Code in CodeSandbox

useQueryState() currently supports following value types: string | number | boolean | Date | string[].

The query string is a global state, so choose the parameter names wisely to prevent accidental clashes. But it is of course allowed to use the same parameter name on purpose when you want access to the same state in multiple places.

If not safe for the URL: useLocationState()

In some cases, you might not want to store the state in the query string of the URL, but still, want the user to be able to restore a previous state using the back/forward actions of the browser. To enable this useLocationState() persists state in the history state instead.

The API works the same. You provide a name and a default value and get the current value, and the update function returned as a pair.

const [currentValue, updateValueFunction] = useLocationState(paramName, defaultValue)
Enter fullscreen mode Exit fullscreen mode

For persisting complex or more sensitive state useLocationState() is more suitable, for example, the state of a comment form. Also, state based on data that frequently changes is better suited to be stored in history.state. This way, you can avoid offering URLs that only work for a short time.

Installation / Usage

You can install these hooks using yarn or npm:

yarn add use-location-state

Import the hooks where you want to use them:

import { useLocationState, useQueryState } from 'use-location-state'

Are you using react-router or another popular router?

For the best experience install one of the router integrations.

yarn add react-router-use-location-state

And use these imports:

import { useLocationState, useQueryState } from 'react-router-use-location-state'

Thanks!

I hope you find this introduction and the library is useful! Happy to discuss enhancements and answer questions 👏

Top comments (0)