The URL: Your App's Most Underutilized State Manager

Aryansh MahatoAryansh Mahato
7 min read

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 vs pushState appropriately

  • Limit 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.

1
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