John Reilly MacGyver turned Dev 🌻❤️ TypeScript / ts-loader / fork-ts-checker-webpack-plugin / DefinitelyTyped: The Movie

useState with URLs: How to persist state with useSearchParams

3 min read 979

useState With URLs Persist State WIth useSearchParams

The React useState Hook is a great way to persist state inside the context of a component in React. This post demonstrates a simple React Hook that stores state in the URL query string, building on top of the React Router useSearchParams Hook.

useState

Usage of the useState Hook looks like this:

const [greeting, setGreeting] = useState('hello world');

// ....

setTotal('hello John'); // will set greeting to 'hello John'

However, there is a disadvantage to using useState; that state is not persistent and not shareable. So if you want someone else to see what you can see in an application, you’re reliant on them carrying out the same actions that got your application into its current state.

Doing that can be time-consuming and error-prone, so wouldn’t it be great if there was a simple way to share state?

A stateful URL

An effective way to share state between users, without needing a backend for persistence, is with the URL. A URL can contain the required state in the form of the route and the query string/search parameters. The search parameters are particularly powerful as they are entirely generic and customizable.

Thanks to the URLSearchParams API, it’s possible to manipulate the query string without round-tripping to the server. This is a primitive upon which we can build; as long as the URL limit (around 2,000 characters) is not exceeded, we’re free to persist state in a URL.

Consider:

https://our-app.com/?greeting=hi

The URL above is storing a single piece of state: the greeting.

Now consider:



https://our-app.com?greeting=hi&name=john

The URL above goes further and stores multiple pieces of state: the greeting and name.

The useSearchParams Hook

If you’re working with React, React Router makes consuming state in the URL, particularly in the form of query string or search parameters, straightforward. It achieves this with the useSearchParams Hook:

import { useSearchParams } from "react-router-dom";

const [searchParams, setSearchParams] = useSearchParams();

const greeting = searchParams.get('greeting');

// ...

setSearchParams({ 'greeting': 'bonjour' }); // will set URL like so https://our-app.com?greeting=bonjour - this value will feed through to anything driven by the URL

This is a great mechanism for persisting state both locally and in a shareable way.

A significant benefit of this approach is that it doesn’t require posting to the server. It’s just using browser APIs like the URLSearchParams API. Changing a query string parameter happens entirely locally and instantaneously.

The useSearchParamsState Hook

What the useSearchParams Hook doesn’t do is maintain other query string or search parameters.

If you are maintaining multiple pieces of state in your application, that will likely mean multiple query string or search parameters. What would be quite useful, then, is a Hook that allows us to update state without losing other state.

Furthermore, it would be great if we didn’t have to first acquire the searchParams object and then manipulate it. It’s time for our useSearchParamsState Hook:

import { useSearchParams } from "react-router-dom";

export function useSearchParamsState(
    searchParamName: string,
    defaultValue: string
): readonly [
    searchParamsState: string,
    setSearchParamsState: (newState: string) => void
] {
    const [searchParams, setSearchParams] = useSearchParams();

    const acquiredSearchParam = searchParams.get(searchParamName);
    const searchParamsState = acquiredSearchParam ?? defaultValue;

    const setSearchParamsState = (newState: string) => {
        const next = Object.assign(
            {},
            [...searchParams.entries()].reduce(
                (o, [key, value]) => ({ ...o, [key]: value }),
                {}
            ),
            { [searchParamName]: newState }
        );
        setSearchParams(next);
    };
    return [searchParamsState, setSearchParamsState];
}

The above Hook can roughly be thought of as useState<string>, but storing that state in the URL.

Let’s think about how it works. When initialized, the Hook takes two parameters:

  • searchParamName: this is the name of the query string parameter where state is persisted
  • defaultValue: if there is no value in the query string, this is the fallback value

The hook then goes on to wrap the useSearchParams hook. It interrogates the searchParams for the supplied searchParamName, and if it isn’t present, falls back to the defaultValue.

The setSearchParamsState method definition looks somewhat complicated, but essentially all it does is get the contents of the existing search parameters and apply the new state for the current property.

It’s probably worth pausing here a second to observe an opinion that’s lurking in this implementation: it is actually valid to have multiple values for the same search parameter. While this is possible, it’s somewhat rare for this to be used; this implementation only allows for a single value for any given parameter, as that is quite useful behavior.

With all this in place, we have a hook that can be used like so:

const [greeting, setGreeting] = useSearchParamsState("greeting", "hello");

The above code returns a greeting value, which is derived from the greeting search parameter. It also returns a setGreeting function, which allows us to set the greeting value. This is the same API as useState, so it should feel idiomatic to React users. Tremendous!

Persisting query strings across your site

Now, we have this exciting mechanism set up that allows us to store state in our URL and, as a consequence, easily share state by sending someone the URL.

What would also be useful is a way to navigate around our site without losing that state. Imagine I have a date range selected and stored in my URL. As I click around from screen to screen, I want to persist that — I don’t want to have to reselect the date range on each screen.

How can we do this? Well, it turns out to be quite easy. All we need is the useLocation Hook and the corresponding location.search property. That represents the query string, so every time we render a link, we just include that like so:

const [location] = useLocation();

return (<Link to={`/my-page${location.search}`}>Page</>)

Now as we navigate around our site, that state will be maintained.

Conclusion

In this post, we’ve created a useSearchParamsState Hook, which allows state to be persisted to URLs for sharing purposes.

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.


More great articles from LogRocket:


Modernize how you debug your React apps — .

John Reilly MacGyver turned Dev 🌻❤️ TypeScript / ts-loader / fork-ts-checker-webpack-plugin / DefinitelyTyped: The Movie

Leave a Reply