
By: Ron Miller | Published: April 18, 2025
10 Do’s and Don’ts of Saving State to URL Parameters
As your web app matures, you’ll want to save some application state to URL params. That has a lot of advantages, like allowing the user to use the browser Back/Forward navigation, making links shareable and bookmarkable, and enabling some lightweight scripting capabilities.
Not all state should be saved to URL params. Some items you’ll want to save to local storage, to a backend DB, or to session storage. Here are a few rules of thumb about which storage mechanism to use:
- URL params – Save navigation state and UI state. For example: current page, selected items, focused tab, etc. URL params are great for deep-linking to a specific piece of content on the page. A nice example is that in GitHub you can share a specific line of code. e.g. https://github.com/facebook/react/blob/main/package.json#L18 will scroll to and highlight line 18.
- Backend database – Save user settings. For example: light/dark theme, language, currency, etc. Application settings that aren’t user-specific should also be saved to a backend DB. For example: feature flags.
- Local storage / IndexedDB – Save user settings for anonymous users (not logged in). You can use it as an easier substitute for a backend database, though it’s less capable because it doesn’t persist across devices or even across different browsers on the same device.
When you do save to URL parameters, there are common best practices and pitfalls you should be aware of.
Best Practices and Pitfalls
- Don’t save sensitive information to URL params – it’s a security risk. URL params are logged in browser history, web servers, analytics tools, and add to your attack surface area. They are also easily shared, which can expose sensitive information by an innocent mistake. There are usually easy ways to avoid that. For example, if you’re a payment app that displays a list of credit cards and you want to persist the selected credit card to the URL, do not save the actual credit card number. Instead, you can save the CC index or hash.
- Don’t save lengthy texts to query parameters. URL param length has a limit depending on the browser. In Chrome it’s ~32,000 characters. In other browsers, it can be less. It’s a very big number, but if you’re planning to save something like a book contents, maybe come up with a different plan. To be on the safe side, keep URLs under ~2000 characters.
- Do encode and decode strings when saving to URL params. The HTTP address uses percent-encoding (aka URL encoding) for special characters. You can use the functions
decodeURIComponent
andencodeURIComponent
in JavaScript to load and save state to the URL. For example,encodeURIComponent("Hello world")
becomesHello%20world
. Another way to encode is using base64, which is good for big payloads that you want to obfuscate from the user.
- Don’t use pushState more than once for each save. Browsers expose the JS functions
window.history.pushState
andwindow.history.replaceState
to change URL params. pushState adds to the history stack, whereas replaceState replaces the current entry. When you want to save the full state of your app to URL params, you might have multiple components with their own state. You can either gather their respective state and then usewindow.history.pushState
once, or you can have each component modify the URL on its own. But if you do that, a common pitfall is for each component to use pushState. If there are two components, that will create two entries in the history stack, which means that if the user clicks “Back” in the browser, they’ll land on an invalid state. What you can do instead is usewindow.history.pushState
once, followed by multiplewindow.history.replaceState
calls.
- Do use lowercase keys to save URL param keys. While not a strict requirement, the best practice is to use lowercase keys. Since the URL is a shareable link, users may modify it for whatever reason, and you can assume there will be mistakes. Someone might write
Tab
instead oftab
, for example. When loading state from URL params, I suggest ignoring casing altogether to prevent issues.
- Do change a setting’s key to force a setting to a new default. That’s useful if you’ve changed the behavior and the old values are irrelevant. For example, let’s say the
ribbon-state
parameter was either “Hidden” or “Visible”, but you’ve added a new mode called “Simplified”, which you want to be the default. To force that, just use a new key instead ofribbon-state
, e.g.ribbon-state2
. When users go to a shared or bookmarked link with the old key, the code will simply ignore it and use the default "Simplified" state.
- Do sanitize values and use default fallbacks when loading URL params on app startup. Values can become invalid because of legacy code or user modification. Remember, the URL params can be stored in someone’s bookmarks for years. To prevent crashes or corrupt state, sanitize values and provide default fallbacks.
- Don’t let URL params load more than once. Otherwise, the user can change the UI only to have it overridden to the previous state by URL parameters. To load only once, I like writing the URL parsing code before my React components and use the values as defaults in
useState
.
const params = new URLSearchParams(window.location.search);
const initialValue = params.get("item");
const myComponent = () => {
const [myItem, setMyItem] = React.useState(initialVale);
// ...
}
- Do keep URL keys and values clear and readable. Just like with regular variable naming, it helps developers work with the code. But since URL params are exposed to your users, it's like an API of sorts—which makes it even more important to use clear, readable names.
- Don’t do real-time URL parameter updates. For example, if the user types text into a “filter” field and you want to save the filter value in the URL, don’t update it with every keystroke—you’ll flood the history stack. Instead, use debounce/throttling to update the query params. As a rule of thumb, update the URL when the filter is applied, which shouldn't be real-time either. It should appy with throttle/debounce or via an explicit user action.
I've been busy for the last week modifying the entire app to be saving navigation state to the URL params and I had to get this off of my chest. In fact, I'm writing this blog post as a replacement for a therapy session. So I hope it was as good for you as it was for me.
Cheers,
Ron