nuqs-zod-adapter
Version:
A Nuqs adapter to safely bind Zod schemas with query parameters using type safety, debouncing and auto-reset support.
159 lines (113 loc) • 4.43 kB
Markdown
# nuqs-zod-adapter
A nuqs adapter for using zod schemas and it's defaults together without having to repeat code unnecessarily
## Fully compatible with `zod` schemas and defaults
### How it will look like:
`yourwebsite.com/search?title=cool+video&category=a`
(All using Zod schemas & default values from them, 0 nuqs validation)
## Features
- ✅ Infer types directly from Zod schema
- ✅ Debouncing feature (completely customizable)
- ✅ Optional reset of other query keys
- ✅ Compatible with all Nuqs options (`shallow`, `history`, etc.)
- ✅ Parses enums, numbers, booleans, unions, defaults... automatically
## Install
```bash
npm install nuqs-zod-adapter
```
## Usage example
Let's say you want to create a filter for a search form which also includes category filtering that syncs with the URL using Nuqs, but with proper type safety, debouncing and optional reset of other keys (e.g., reset page on title change) and without having to duplicate the defaults
### 1. Define your Zod schema
```typescript
import { z } from 'zod'
export const paramsFields = {
title: z.string().optional(),
category: z.enum(['a', 'b', 'c']).default('a'),
}
```
- **Title:** It's optional but not required, which means that it might not exist in the query
- **Category:** Also optional, however its default value will be A when the query is empty
### 2. Use the hook to connect a param to the URL
Once your schema is defined, you can plug individual fields into the useSafeQueryStateFromZod hook. Here's how to do it for the category param:
```typescript
import { useSafeQueryStateFromZod } from 'nuqs-zod-adapter'
import { paramsFields } from './schema'
const [category, setCategory] = useSafeQueryStateFromZod(
'category',
paramsFields.category,
{
delay: 200, // debounce delay (optional)
resetKeys: ['page'], // reset other query params when this changes
shallow: true // use shallow routing
// and all the nuqs options available
}
)
```
✅ Syncs category with the query string
✅ Applies Zod validation and type inference
✅ Uses "a" as the default value if none is provided
✅ Debounces updates to avoid router spam
✅ Optionally resets other keys like "page"
### 3. Using it in your component
Once you have `category` and `setCategory`, you can bind them directly to a `<select>` input, radio buttons, or any other component like this:
```typescript
<select
value={category}
onChange={(e) => setCategory(e.target.value as typeof category)}
className="border px-3 py-2 rounded"
>
<option value="a">Category A</option>
<option value="b">Category B</option>
<option value="c">Category C</option>
</select>
```
> ⚠️ Make sure to cast e.target.value to the correct type (e.g. as typeof category) if TypeScript complains.
### 4. Combine multiple query params
You can use the hook multiple times, once per field, to build a full filter system connected to the URL
#### Category & Title together
```typescript
const [title, setTitle] = useSafeQueryStateFromZod(
'title',
paramsFields.title,
{
delay: 300,
resetKeys: ['page'],
shallow: true
}
)
const [category, setCategory] = useSafeQueryStateFromZod(
'category',
paramsFields.category,
{
shallow: true
}
)
```
Now they will be both synced, if you type a title on the form, it will display on the URL like this:
**If there is no title:**
- `yourwebsite.com`
**If there is a title:**
- `yourwebsite.com?title=super+title`
**If there is a title AND a category:**
- `yourwebsite.com?title=super+title&category=a`
**If there is only a category:**
- `yourwebsite.com?category=a`
## Resetting the values
What if you want to reset all the values with a button? Well, it's as simple as adding a "onClick" function and using the exported setters to set the values to their defaults, here is an example:
```typescript
<button
onClick={() => {
setTitle('')
setCategory('A') // or paramsFields.category._def.defaultValue() if you want to extract it
}}
className="px-4 py-2 bg-gray-200 rounded hover:bg-gray-300"
>
Reset Filters
</button>
```
As simple as that.
## 🛠 Contributing
Contributions are welcome! If you have improvements, bug fixes or new ideas, feel free to submit a pull request or open an issue.
## 👤 Author
**Diego Rodríguez**
[LinkedIn](https://www.linkedin.com/in/bydiego/)
[GitHub](https://github.com/dewstouh)