useState
with URLs: How to 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
HookUsage of the useState
Hook looks like this:
const [greeting, setGreeting] = useState('hello world'); // .... setGreeting('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 others 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?
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 above URL is storing a single piece of state: the greeting
.
Now consider:
https://our-app.com?greeting=hi&name=john
The above URL goes further and stores multiple pieces of state: the greeting
and name
.
useSearchParams
HookIf you’re working with React, React Router makes consuming state in the URL, particularly in the form of a 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.
useSearchParamsState
HookWhat the useSearchParams
Hook doesn’t do is maintain other query string or search parameters.
If we 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 states.
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 state in the URL.
Let’s think about how it works. When initialized, the Hook takes two parameters:
searchParamName
: the name of the query string parameter where state is persisteddefaultValue
: the fallback value if there’s no value in the query stringThe 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!
useSearchParamsState
Hook vs. useSearchParams
HookAt this point, you might be wondering “why don’t we always use the useSearchParamsState
Hook?” It all boils down to one reason: performance. The useSearchParamsState
Hook is slower than the useState
Hook. Let’s think about why.
If we’re using the useState
Hook, then ultimately a variable is being updated inside the program that represents our application. This is internal state. However, the story is slightly different for the useSearchParamsState
Hook.
The useSearchParamsState
Hook is built upon the useSearchParams
Hook in React Router, as we’ve seen. If we look at the implementation of that Hook, we can see that it relies on various browser APIs, such as location
and History
:
The upshot here is that the state of the useSearchParamsState
Hook is external
to our application. It may not feel this way because we haven’t had to set up a database or an API, its state is external. State lives in the browser’s APIs, and with that comes a performance penalty. Here’s what happens every time we change state:
useSearchParams
Hook in React Router invokes the History
APIlocation.search
and surfaces a new value for the applicationThe above is slower than just invoking useState
and relying upon a local variable. However, it’s not overwhelmingly slower. I’ve generally not had an issue because browsers are very fast these days. Still, if you’re intending to write code that is as performant as possible you may want to avoid this Hook.
Anything that involves an external API, even if it’s an API that lives in the browser, will be slower than local variables. I would expect there to be very few applications for which this is a significant factor, but it is still worth considering.
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.
In this post, we’ve created a useSearchParamsState
Hook, which allows state to be persisted to URLs for sharing purposes.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowThe useReducer React Hook is a good alternative to tools like Redux, Recoil, or MobX.
Node.js v22.5.0 introduced a native SQLite module, which is is similar to what other JavaScript runtimes like Deno and Bun already have.
Understanding and supporting pinch, text, and browser zoom significantly enhances the user experience. Let’s explore a few ways to do so.
Playwright is a popular framework for automating and testing web applications across multiple browsers in JavaScript, Python, Java, and C#. […]
5 Replies to "<code>useState</code> with URLs: How to persist state with <code>useSearchParams</code>"
You can update search params easily with this approach, using native useSearchParams functional update.
setSearchParams((searchParams) => {
searchParams.set(“greeting”, “hi”);
return searchParams;
});
Hi,
Great post!
It would be nice to see a section on how to manage updating multiple keys at the same time.
For example if you set State A then State B you will end up with only State B’s changes.
In my case I am trying to update a start and end date that are updated by the same callback function.
Cheers,
Casey
The first example of useState calls setTotal instead of setGreeting
Thanks for the heads-up, this has been fixed
Created a library to simplify storing state in URL https://www.reddit.com/r/nextjs/comments/17d4x2k/about_using_url_for_managing_state_in_nextjs_13/