Type-Safe URL State in React with nuqs (and Zod)
Managing URL query parameters in modern React applications is surprisingly tricky.
Pagination, filters, sorting, tabs, search. All of these often live in the URL. That's great for shareability and persistence, but the developer experience quickly becomes painful.
- Everything is a string
- Parsing logic is duplicated
- Defaults are inconsistent
- Invalid values cause bugs
- Synchronizing state with the router is tedious
After repeatedly re-implementing the same logic across projects, I discovered nuqs, a small library that treats the URL as a first-class state container.
This article explains what nuqs is, why it's useful, and how to integrate it with Zod for runtime validation and type safety.
The Problem with Query Params
Most codebases start with something like this:
const page = Number(searchParams.get('page') ?? 1);
const sort = searchParams.get('sort') ?? 'asc';
const showArchived = searchParams.get('archived') === 'true';It works, but it comes with hidden issues:
Number()can produceNaN- Invalid values are silently accepted
- Defaults are repeated everywhere
- No type safety
- Hard to keep consistent across pages
Over time, this becomes technical debt.
What we really want is something closer to React state.
Enter nuqs
nuqs is a small library for managing URL query state in React and Next.js applications.
Instead of manually reading and writing query parameters, you use hooks that feel similar to useState.
const [page, setPage] = useQueryState(
'page',
parseAsInteger.withDefault(1)
);Updating state automatically updates the URL:
setPage(2);?page=2The key idea is simple but powerful: treat the URL as state.
Why This Matters
Using the URL as state provides several benefits:
- Shareable application state
- Persistence across reloads
- Browser navigation support
- Single source of truth
- Predictable behavior
Without tooling, the developer experience is poor. nuqs solves that gap.
Basic Example
Here's a simple pagination example:
import { useQueryState, parseAsInteger } from 'nuqs';
export function Pagination() {
const [page, setPage] = useQueryState(
'page',
parseAsInteger.withDefault(1)
);
return (
<button onClick={() => setPage(page + 1)}>
Next page ({page})
</button>
);
}No manual parsing. No router logic. Just state.
Validation with Zod
One important insight: query parameters are user input. Users can edit URLs manually, so validation matters.
If your project already uses Zod, integrating it with nuqs is straightforward.
import { z } from 'zod';
import { createParser } from 'nuqs';
const sortSchema = z.enum(['asc', 'desc']);
const sortParser = createParser({
parse: (value) => sortSchema.parse(value),
serialize: (value) => value,
});Then use it like any other parser:
const [sort, setSort] = useQueryState(
'sort',
sortParser.withDefault('asc')
);Benefits:
- Runtime validation
- Type inference
- Shared schemas across the app
- Safer defaults
Real-World Use Cases
nuqs works particularly well for:
- Pagination
- Table filters
- Search queries
- Sorting
- Tabs
- Feature flags
- Dashboard state
nuqs vs query-string / qs
Libraries like query-string or qs solve a different problem. They convert objects to and from query strings. nuqs focuses on synchronizing app state with the URL.
| Feature | query-string / qs | nuqs |
|---|---|---|
| Parse & stringify | Yes | Basic |
| React integration | No | Yes |
| State synchronization | No | Yes |
| Defaults | No | Yes |
| Type safety | No | Yes |
| Validation integration | Manual | Natural |
When NOT to Use nuqs
Like any tool, nuqs can be overused. Avoid it when:
- State is purely local UI
- Data is large or complex
- URL readability matters more than convenience
- Performance constraints are extreme
Lessons Learned
After using nuqs in production, the biggest improvements were:
- Less boilerplate
- Fewer bugs from invalid params
- More predictable behavior
- Better developer experience
- Consistent patterns across the codebase
Final Thoughts
Managing query parameters manually is easy at first until it isn't. nuqs provides a pragmatic abstraction that makes URL state predictable, type-safe, and ergonomic in React applications.
It's not the only solution, but if your app relies heavily on URL-driven state, it's worth exploring.
Resources
- nuqs GitHub: https://github.com/47ng/nuqs
- Zod: https://github.com/colinhacks/zod
If you deal with query parameters often, nuqs might save you a surprising amount of time and bugs.
