The URL: Your App's Most Underutilized State Manager


In the world of modern web development, we often reach for complex state management solutions like Redux, Zustand, or MobX without considering one of the most powerful and overlooked state managers that's been with us since the dawn of the web: the URL itself.
The URL isn't just an address—it's a stateful, shareable, bookmarkable, and SEO-friendly state container that can dramatically simplify your application architecture while improving user experience.
Why to use URL as a state manager?
How many times you wanted to manage state such as filters, sorting, modal open/closed? Instead of storing it as React state
or Angular Signals
store it in URL as Query param, it will be easy, accessible in multiple components and most importantly, shareable.
What do I mean by shareable?
Suppose you're building a table with tons of filters and your users want to share those specific filtered views with their teammates. You could build some fancy "Save Filters" feature with databases and user accounts, but here's a much simpler solution: just use the damn query parameters. Store all your applied filters right in the URL like /reports?status=active&category=sales&sortBy=revenue&page=2
. Now when users share that link, boom—all the filters are already baked into the URL and get copied along with it. The other person clicks the link and doesn't have to mess around reapplying the same filters, and you don't have to waste time building some overcomplicated filter-saving system. Easy sharing, zero extra work. Win-win.
Where to store state in URL?
There are 2 places where you can store the state, and it’s very important where you store it. Store it as Path Param or Query Param.
Path Parameters
Path parameters should be used for required values that are essential for the page to function properly. These represent core resource identifiers or mandatory data without which the page cannot render meaningful content.
Example:
/products/:id
In this route, the id
parameter is essential for displaying product details. Without it, the application cannot determine which product to render, making the page non-functional.
Query Parameters
Query parameters should be used for optional values that enhance or modify the default behavior of a page without preventing it from functioning. These represent filters, sorting options, pagination settings, or other supplementary configurations.
Example:
/products?sort=name,asc&limit=20&category=electronics
In this route, query parameters like sort
, limit
, and category
are optional. The products page will render successfully without them, applying sensible defaults (e.g., default sorting, standard page size, showing all categories).
What Belongs in URL State?
Not everything should live in the URL, but these types of state are perfect candidates:
Filters and search queries
Pagination and sorting parameters
Active tabs or views
Modal or dialog states
Form draft states
Map coordinates and zoom levels
Date ranges and time periods
Practical Implementations
Vanilla JavaScript
// Reading URL state
const urlParams = new URLSearchParams(window.location.search);
const currentFilter = urlParams.get('filter') || 'all';
const currentPage = parseInt(urlParams.get('page')) || 1;
// Updating URL state
function updateURLState(key, value) {
const url = new URL(window.location);
url.searchParams.set(key, value);
window.history.pushState({}, '', url);
}
React with Custom Hook
import { useState, useEffect } from 'react';
function useURLState(key, defaultValue) {
const [state, setState] = useState(() => {
const params = new URLSearchParams(window.location.search);
return params.get(key) || defaultValue;
});
const updateState = (value) => {
setState(value);
const url = new URL(window.location);
if (value) {
url.searchParams.set(key, value);
} else {
url.searchParams.delete(key);
}
window.history.replaceState({}, '', url);
};
// Listen for browser back/forward button changes
useEffect(() => {
const handlePopState = () => {
const params = new URLSearchParams(window.location.search);
setState(params.get(key) || defaultValue);
};
window.addEventListener('popstate', handlePopState);
return () => window.removeEventListener('popstate', handlePopState);
}, [key, defaultValue]);
return [state, updateState];
}
// Usage example
function ProductFilter() {
const [category, setCategory] = useURLState('category', 'all');
const [priceRange, setPriceRange] = useURLState('price', '0-1000');
return (
<div>
<select value={category} onChange={(e) => setCategory(e.target.value)}>
<option value="all">All Categories</option>
<option value="electronics">Electronics</option>
<option value="clothing">Clothing</option>
</select>
</div>
);
}
Angular with Service
import { Injectable } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { BehaviorSubject, Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class URLStateService {
constructor(
private router: Router,
private route: ActivatedRoute
) {}
getStateParam(key: string, defaultValue: string = ''): string {
return this.route.snapshot.queryParams[key] || defaultValue;
}
updateStateParam(key: string, value: string): void {
const queryParams = { ...this.route.snapshot.queryParams };
if (value) {
queryParams[key] = value;
} else {
delete queryParams[key];
}
this.router.navigate([], {
relativeTo: this.route,
queryParams,
queryParamsHandling: 'replace'
});
}
watchStateParam(key: string, defaultValue: string = ''): Observable<string> {
const subject = new BehaviorSubject<string>(
this.getStateParam(key, defaultValue)
);
// Subscribe to route changes
this.route.queryParams.subscribe(params => {
subject.next(params[key] || defaultValue);
});
return subject.asObservable();
}
}
// Component usage example
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-product-filter',
template: `
<div>
<select [value]="category" (change)="onCategoryChange($event)">
<option value="all">All Categories</option>
<option value="electronics">Electronics</option>
<option value="clothing">Clothing</option>
</select>
<input
[value]="searchTerm"
(input)="onSearchChange($event)"
placeholder="Search products..."
/>
</div>
`
})
export class ProductFilterComponent implements OnInit {
category: string = 'all';
searchTerm: string = '';
constructor(private urlStateService: URLStateService) {}
ngOnInit() {
// Initialize from URL
this.category = this.urlStateService.getStateParam('category', 'all');
this.searchTerm = this.urlStateService.getStateParam('search', '');
// Watch for changes (back/forward navigation)
this.urlStateService.watchStateParam('category', 'all')
.subscribe(value => this.category = value);
this.urlStateService.watchStateParam('search', '')
.subscribe(value => this.searchTerm = value);
}
onCategoryChange(event: Event) {
const target = event.target as HTMLSelectElement;
this.category = target.value;
this.urlStateService.updateStateParam('category', target.value);
}
onSearchChange(event: Event) {
const target = event.target as HTMLInputElement;
this.searchTerm = target.value;
this.urlStateService.updateStateParam('search', target.value);
}
}
Why URLs Make Excellent State Managers
1. Inherent Persistence
Unlike in-memory state that disappears on page refresh, URL state persists across browser sessions. Users can bookmark specific application states, share them with colleagues, or return to exactly where they left off.
2. Zero Configuration Required
Every web application already has URL state management built-in. No additional libraries, no setup, no configuration—just leverage what's already there.
3. Universal Shareability
Want to share your current filtered view of a data table? Just copy the URL. This natural shareability is something that complex state managers struggle to replicate.
4. SEO Benefits
Search engines can crawl and index different URL states, making your application more discoverable. Each URL state becomes a potential landing page.
5. Browser History Integration
Users expect the back button to work. URL-based state naturally integrates with browser history, providing intuitive navigation.
Real-World Examples
E-commerce Product Filtering
/products?category=electronics&price_min=100&price_max=500&sort=rating&page=2
This URL tells a complete story: showing electronics between $100-500, sorted by rating, on page 2.
Analytics Dashboard
/dashboard/analytics#view=revenue&timeframe=30d&breakdown=channel&chart=bar
Every parameter represents a user choice that affects the dashboard view.
Data Table State
/users?search=john&status=active&sort=created_desc&limit=50&offset=100
The entire table state—search, filters, sorting, and pagination—lives in the URL.
Best Practices
1. Keep URLs Human-Readable
✅ /reports?period=2024-q1&metric=revenue
❌ /reports?p=0x2f&m=rv
2. Use Semantic Parameter Names
✅ ?category=electronics&sort=price_asc
❌ ?c=elec&s=pa
3. Provide Sensible Defaults
Always have fallback values for missing parameters to ensure your app works with incomplete URLs.
4. Validate URL Parameters
Never trust URL input—validate and sanitize all parameters before using them in your application logic. If you are rendering the value in frontend then your framework should handle this, but if you are sending the param data to backend then backend should have validation to validate these things with help of zod or other validation library.
5. Consider URL Length Limits
While modern browsers support very long URLs, keep them reasonable for shareability and readability.
When NOT to Use URL State
URL state isn't appropriate for:
Sensitive information (passwords, API keys)
Large datasets (entire table contents)
Temporary UI states (loading indicators, hover states)
User session data (authentication tokens)
Frequently changing values (real-time counters)
Performance Considerations
URL state management is generally very performant, but consider:
Debounce rapid updates to prevent history pollution
Use
replaceState
vspushState
appropriatelyLimit serialized object size to keep URLs manageable
Cache parsed parameters to avoid repeated parsing
Seems like a silver bullet?
URL as a state manager seems good right for all the scenarios? Nope! As with all the technologies here comes a catch, it’s not persistent if user loses the link. What if user have to apply same/similar filter again and again? You can’t expect user to save the link of the applied filters. If you want that users should be able to save the filters linked with their account then you’ll have to implement Save Filters
feature.
Conclusion
The URL is more than just an address—it's a powerful, built-in state management solution that's been hiding in plain sight. By leveraging URLs for appropriate state, you can build applications that are more shareable, bookmarkable, and user-friendly while often reducing complexity.
Before reaching for that next state management library, ask yourself: "Could this live in the URL?" You might be surprised how often the answer is yes.
The best state manager might just be the one that's been with us all along—sitting right there in the browser's address bar, waiting to be fully utilized.
Subscribe to my newsletter
Read articles from Aryansh Mahato directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Aryansh Mahato
Aryansh Mahato
Lead web developer with experience of more that 7 years. React.js, Next.js, Bootstrap, Material UI, React Query Node.js, Express.js, Nest.js, TypScript GoLang, Gin, GraphQL, JWT PostgreSQL, MongoDB, Redis, ElasticSearch Docker, Kubernetes, Kong, Istio AWS, Azure, Vercel, Netlify Strapi, Sanity, Firebase, Supabase Git, GitHub, GitLab, CircleCI