https://alfy.blog/2025/10/31/your-url-is-your-state.html Ahmad Alfy Blog about front-end development and the web * Blog RSS feed * About * Contact Your URL Is Your State 31 Oct 2025 Couple of weeks ago when I was publishing The Hidden Cost of URL Design I needed to add SQL syntax highlighting. I headed to PrismJS website trying to remember if it should be added as a plugin or what. I was overwhelmed with the amount of options in the download page so I headed back to my code. I checked the file for PrismJS and at the top of the file, I found a comment containing a URL: /* https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript+bash+css-extras+markdown+scss+sql&plugins=line-highlight+line-numbers+autolinker */ I had completely forgotten about this. I clicked the URL, and it was the PrismJS download page with every checkbox, dropdown, and option pre-selected to match my exact configuration. Themes chosen. Languages selected. Plugins enabled. Everything, perfectly reconstructed from that single URL. It was one of those moments where something you once knew suddenly clicks again with fresh significance. Here was a URL doing far more than just pointing to a page. It was storing state, encoding intent, and making my entire setup shareable and recoverable. No database. No cookies. No localStorage. Just a URL. This got me thinking: how often do we, as frontend engineers, overlook the URL as a state management tool? We reach for all sorts of abstractions to manage state such as global stores, contexts, and caches while ignoring one of the web's most elegant and oldest features: the humble URL. In my previous article, I wrote about the hidden costs of bad URL design. Today, I want to flip that perspective and talk about the immense value of good URL design. Specifically, how URLs can be treated as first-class state containers in modern web applications. The Overlooked Power of URLs Scott Hanselman famously said "URLs are UI" and he's absolutely right. URLs aren't just technical addresses that browsers use to fetch resources. They're interfaces. They're part of the user experience. But URLs are more than UI. They're state containers. Every time you craft a URL, you're making decisions about what information to preserve, what to make shareable, and what to make bookmarkable. Think about what URLs give us for free: * Shareability: Send someone a link, and they see exactly what you see * Bookmarkability: Save a URL, and you've saved a moment in time * Browser history: The back button just works * Deep linking: Jump directly into a specific application state URLs make web applications resilient and predictable. They're the web's original state management solution, and they've been working reliably since 1991. The question isn't whether URLs can store state. It's whether we're using them to their full potential. Before we dive into examples, let's break down how URLs encode state. Here's a typical stateful URL: Anatomy of URL Anatomy of a URL - Source: What is a URL - MDN Web Docs For many years, these were considered the only components of a URL. That changed with the introduction of Text Fragments, a feature that allows linking directly to a specific piece of text within a page. You can read more about it in my article Smarter than 'Ctrl+F': Linking Directly to Web Page Content. Different parts of the URL encode different types of state: 1. Path Segments (/path/to/myfile.html). Best used for hierarchical resource navigation: + /users/123/posts - User 123's posts + /docs/api/authentication - Documentation structure + /dashboard/analytics - Application sections 2. Query Parameters (?key1=value1&key2=value2). Perfect for filters, options, and configuration: + ?theme=dark&lang=en - UI preferences + ?page=2&limit=20 - Pagination + ?status=active&sort=date - Data filtering + ?from=2025-01-01&to=2025-12-31 - Date ranges 3. [DEL:Anchor:DEL] Fragment (#SomewhereInTheDocument). Ideal for client-side navigation and page sections: + #L20-L35 - GitHub line highlighting + #features - Scroll to section + #/dashboard - Single-page app routing (though it's rarely used these days) Common Patterns That Work for Query Parameters Multiple values with delimiters Sometimes you'll see multiple values packed into a single key using delimiters like commas or plus signs. It's compact and human-readable, though it requires manual parsing on the server side. ?languages=javascript+typescript+python ?tags=frontend,react,hooks Nested or structured data Developers often encode complex filters or configuration objects into a single query string. A simple convention uses key-value pairs separated by commas, while others serialize JSON or even Base64-encode it for safety. ?filters=status:active,owner:me,priority:high ?config=eyJyaWNrIjoicm9sbCJ9== (base64-encoded JSON) Boolean flags For flags or toggles, it's common to pass booleans explicitly or to rely on the key's presence as truthy. This keeps URLs shorter and makes toggling features easy. ?debug=true&analytics=false ?mobile (presence = true) Arrays (Bracket notation) ?tags[]=frontend&tags[]=react&tags[]=hooks Another old pattern is bracket notation, which represents arrays in query parameters. It originated from early web frameworks like PHP where appending [] to a parameter name signals that multiple values should be grouped together. ?tags[]=frontend&tags[]=react&tags[]=hooks ?ids[0]=42&ids[1]=73 Many modern frameworks and parsers (like Node's qs library or Express middleware) still recognize this pattern automatically. However, it's not officially standardized in the URL specification, so behavior can vary depending on the server or client implementation. Notice how it even breaks the syntax highlighting on my website. The key is consistency. Pick patterns that make sense for your application and stick with them. State via URL Parameters Let's look at real-world examples of URLs as state containers: PrismJS Configuration https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript&plugins=line-numbers The entire syntax highlighter configuration encoded in the URL. Change anything in the UI, and the URL updates. Share the URL, and someone else gets your exact setup. This one uses anchor and not query parameters, but the concept is the same. GitHub Line Highlighting https://github.com/zepouet/Xee-xCode-4.5/blob/master/XeePhotoshopLoader.m#L108-L136 It links to a specific file while highlighting lines 108 through 136. Click this link anywhere, and you'll land on the exact code section being discussed. Google Maps https://www.google.com/maps/@22.443842,-74.220744,19z Coordinates, zoom level, and map type all in the URL. Share this link, and anyone can see the exact same view of the map. Figma and Design Tools https://www.figma.com/file/abc123/MyDesign?node-id=123:456&viewport=100,200,0.5 Before shareable design links, finding an updated screen or component in a large file was a chore. Someone had to literally show you where it lived, scrolling and zooming across layers. Today, a Figma link carries all that context like canvas position, zoom level, selected element. Literally everything needed to drop you right into the workspace. E-commerce Filters https://store.com/laptops?brand=dell+hp&price=500-1500&rating=4&sort=price-asc This is one of the most common real-world patterns you'll encounter. Every filter, sort option, and price range preserved. Users can bookmark their exact search criteria and return to it anytime. Most importantly, they can come back to it after navigating away or refreshing the page. Frontend Engineering Patterns Before we discuss implementation details, we need to establish a clear guideline for what should go into the URL. Not all state belongs in URLs. Here's a simple heuristic: Good candidates for URL state: * Search queries and filters * Pagination and sorting * View modes (list/grid, dark/light) * Date ranges and time periods * Selected items or active tabs * UI configuration that affects content * Feature flags and A/B test variants Poor candidates for URL state: * Sensitive information (passwords, tokens, PII) * Temporary UI states (modal open/closed, dropdown expanded) * Form input in progress (unsaved changes) * Extremely large or complex nested data * High-frequency transient states (mouse position, scroll position) If you are not sure if a piece of state belongs in the URL, ask yourself: If someone else clicking this URL, should they see the same state? If so, it belongs in the URL. If not, use a different state management approach. Implementation using Plain JavaScript The modern URLSearchParams API makes URL state management straightforward: // Reading URL parameters const params = new URLSearchParams(window.location.search); const view = params.get('view') || 'grid'; const page = params.get('page') || 1; // Updating URL parameters function updateFilters(filters) { const params = new URLSearchParams(window.location.search); // Update individual parameters params.set('status', filters.status); params.set('sort', filters.sort); // Update URL without page reload const newUrl = `${window.location.pathname}?${params.toString()}`; window.history.pushState({}, '', newUrl); // Now update your UI based on the new filters renderContent(filters); } // Handling back/forward buttons window.addEventListener('popstate', () => { const params = new URLSearchParams(window.location.search); const filters = { status: params.get('status') || 'all', sort: params.get('sort') || 'date' }; renderContent(filters); }); The popstate event fires when the user navigates with the browser's Back or Forward buttons. It lets you restore the UI to match the URL, which is essential for keeping your app's state and history in sync. Usually your framework's router handles this for you, but it's good to know how it works under the hood. Implementation using React React Router and Next.js provide hooks that make this even cleaner: import { useSearchParams } from 'react-router-dom'; // or for Next.js 13+: import { useSearchParams } from 'next/navigation'; function ProductList() { const [searchParams, setSearchParams] = useSearchParams(); // Read from URL (with defaults) const color = searchParams.get('color') || 'all'; const sort = searchParams.get('sort') || 'price'; // Update URL const handleColorChange = (newColor) => { setSearchParams(prev => { const params = new URLSearchParams(prev); params.set('color', newColor); return params; }); }; return (