DEV Community

Zeeshan Haider Shaheen
Zeeshan Haider Shaheen

Posted on • Edited on

Stop Fighting URL State in React: Introducing react-zod-url-state

Ever built a search page with filters and pagination, only to realize users lose everything when they refresh? Or tried to share a filtered view with a colleague and sent them a useless base URL?

I've been there. Too many times.

After writing the same URLSearchParams boilerplate for the hundredth time, I decided to build something better: react-zod-url-state - a TypeScript-first library that automatically syncs your component state with URL parameters.

The Problem: URL State is Painful

Here's what we usually end up writing for a simple product filter:

export function ProductFilters() {
  const searchParams = useSearchParams();
  const router = useRouter();
  const [q, setQ] = useState('');
  const [page, setPage] = useState(1);
  const [sort, setSort] = useState('name');
  const [inStock, setInStock] = useState(false);

  // Sync URL to state on mount
  useEffect(() => {
    setQ(searchParams.get('q') || '');
    setPage(parseInt(searchParams.get('page')) || 1);
    setSort(searchParams.get('sort') || 'name');
    setInStock(searchParams.get('inStock') === 'true');
  }, [searchParams]);

  // Sync state to URL
  const updateUrl = useCallback((updates) => {
    const params = new URLSearchParams(searchParams);
    Object.entries(updates).forEach(([key, value]) => {
      if (value === undefined || value === '') {
        params.delete(key);
      } else {
        params.set(key, String(value));
      }
    });
    router.replace(`?${params.toString()}`);
  }, [searchParams, router]);

  const handleSearch = (value) => {
    setQ(value);
    updateUrl({ q: value, page: 1 });
  };

  // ... more handlers
}
Enter fullscreen mode Exit fullscreen mode

That's 50+ lines for basic URL state management. And we haven't even handled arrays, dates, or validation yet!

The Solution: Declarative URL State

With react-zod-url-state, the same functionality becomes:

import { defineUrlState, useUrlState, z } from "react-zod-url-state";

const filters = defineUrlState(z.object({
  q: z.string().default(""),
  page: z.number().int().min(1).default(1),
  sort: z.enum(["name", "price"]).default("name"),
  inStock: z.boolean().default(false),
}));

export function ProductFilters() {
  const [state, setState] = useUrlState(filters);

  return (
    <div>
      <input
        value={state.q}
        onChange={(e) => setState({ 
          q: e.target.value, 
          page: 1 
        })}
      />
      <select
        value={state.sort}
        onChange={(e) => setState({ 
          sort: e.target.value 
        })}
      >
        <option value="name">Name</option>
        <option value="price">Price</option>
      </select>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Result: 85% less code, automatic type safety, zero boilerplate.

Key Features

🔒 Type Safety with Zod

Define your URL state schema once, get TypeScript validation everywhere:

const filters = defineUrlState(z.object({
  categories: z.array(z.string()).default([]),
  dateRange: z.object({
    from: z.date(),
    to: z.date()
  }).optional(),
  price: z.object({
    min: z.number(),
    max: z.number()
  }).default({ min: 0, max: 1000 })
}));
Enter fullscreen mode Exit fullscreen mode

🎯 Automatic Serialization

Handles complex data types automatically:

  • Arrays: categories=shoes,boots
  • Booleans: inStock=true
  • Numbers: page=2&price=100
  • Dates: startDate=2024-01-15
  • Enums: sort=price

Debounced Updates

Prevent URL spam with built-in debouncing:

const filters = defineUrlState(schema, {
  debounceMs: 300, // Wait 300ms before updating URL
  mode: "replace"  // Don't create history entries
});
Enter fullscreen mode Exit fullscreen mode

🔗 Share State Easily

Generate shareable URLs with one line:

const { copyShareLink } = useShareLink(filters);

<button onClick={() => copyShareLink()}>
  Share Current Filters
</button>
Enter fullscreen mode Exit fullscreen mode

🔄 Reset to Defaults

Clear all filters instantly:

const resetState = useResetUrlState(filters);

<button onClick={resetState}>
  Clear All Filters
</button>
Enter fullscreen mode Exit fullscreen mode

Framework Support

Works seamlessly with your favorite React framework:

Next.js (App Router & Pages Router)

const [state, setState] = useNextUrlState(filters);
Enter fullscreen mode Exit fullscreen mode

React Router

const [state, setState] = useReactRouterUrlState(filters);
Enter fullscreen mode Exit fullscreen mode

Server-Side Rendering

// Next.js App Router
export default function Page({ searchParams }) {
  const state = filters.readFrom(searchParams);
  // Use state for data fetching
}

// React Router loader
export async function loader({ request }) {
  const state = getStateFromLoader(filters, request);
  // Use state for data fetching
}
Enter fullscreen mode Exit fullscreen mode

Real-World Use Cases

Perfect for:

  • 🔍 Search interfaces with filters and sorting
  • 📊 Data tables with pagination and column sorting
  • 🎛️ Dashboard filters that users need to bookmark
  • 📈 Analytics views where state sharing is crucial
  • 🛍️ E-commerce product catalogs

Getting Started

npm install react-zod-url-state
Enter fullscreen mode Exit fullscreen mode

The library has zero dependencies besides React and Zod, and works with all modern React patterns including concurrent features.

Why I Built This

After years of copying and pasting URL state management code across projects, I realized we needed a better abstraction. The React community has amazing solutions for form state (React Hook Form), global state (Zustand, Redux), but URL state remained stuck in the stone age.

react-zod-url-state brings the same declarative, type-safe approach to URL state that we expect from modern React development.

Try It Out

Check out the interactive demo or install it in your project:

npm install react-zod-url-state
Enter fullscreen mode Exit fullscreen mode

The npm package has more examples and advanced usage patterns.


What's your current approach to URL state management? Have you run into similar pain points? I'd love to hear your thoughts and feedback!

If this solved a problem for you, consider giving it a ⭐ on GitHub - it helps other developers discover the library.

Top comments (5)

Collapse
 
franky47 profile image
François Best

Hi, I'm the author of nuqs which solves a similar pain point, I really like your auto-serialisation based on the schema type, this is a thing that's missing in schema validation libraries.

Great job there, those URLs are pretty neat.

Collapse
 
parag_nandy_roy profile image
Parag Nandy Roy

This is 🔥 finally a way to stop writing the same URL boilerplate over and over...

Collapse
 
westernal profile image
Ali Navidi

Great job!

Collapse
 
zeeshanhshaheen profile image
Zeeshan Haider Shaheen

Thanks a lot. If you like it please give it a star on GitHub :)

Collapse
 
shrinithi profile image
Shri Nithi

Great One!