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
}
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>
);
}
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 })
}));
🎯 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
});
🔗 Share State Easily
Generate shareable URLs with one line:
const { copyShareLink } = useShareLink(filters);
<button onClick={() => copyShareLink()}>
Share Current Filters
</button>
🔄 Reset to Defaults
Clear all filters instantly:
const resetState = useResetUrlState(filters);
<button onClick={resetState}>
Clear All Filters
</button>
Framework Support
Works seamlessly with your favorite React framework:
Next.js (App Router & Pages Router)
const [state, setState] = useNextUrlState(filters);
React Router
const [state, setState] = useReactRouterUrlState(filters);
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
}
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
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
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)
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.
This is 🔥 finally a way to stop writing the same URL boilerplate over and over...
Great job!
Thanks a lot. If you like it please give it a star on GitHub :)
Great One!