use-query-guard
Version:
A router-agnostic query-string management hook for React with Zod validation
178 lines (136 loc) • 4.59 kB
Markdown
# useQueryGuard
[](https://www.npmjs.com/package/use-query-guard)
[](https://github.com/sakuyasann/use-query-guard)
A **router‑agnostic query‑string management hook** for React.
Read, validate, and update URL parameters in **React‑Router, Next.js, Remix, or any vanilla SPA** with one shared API.
## Features
- **Router‑free core**
Uses `window.history` by default. Inject a custom *adapter* to integrate any router.
- **Type‑safe validation with Zod**
Pass a `z.object()` schema and `data` is returned with that exact type. Automatic coercion (`"42"` → `42`, etc.) included.
- **Safe updates**
`updateParams()` only accepts keys declared in the schema (if provided) and values of type `string | number | boolean | null`.
- **`null` to delete keys**
`undefined` means "leave unchanged"; `null` removes the key from the URL.
- **SSR friendly**
All browser APIs wrapped in `typeof window` checks.
## Installation
```bash
npm install use-query-guard zod
# or
pnpm add use-query-guard zod
# or
yarn add use-query-guard zod
```
**Alternative**: Copy the hook code directly to your project:
```bash
# Copy the hook code to src/hooks/useSearchParams.ts
```
Dependencies: **React 18+** and **Zod 3+** only.
## Quick start
```tsx
import { useQueryGuard } from 'use-query-guard'
import { z } from 'zod'
const schema = z.object({
page: z.number(),
q: z.string().optional(),
vip: z.boolean(),
})
export default function Products() {
const { data, isReady, isError, updateParams } =
useQueryGuard({ resolver: schema })
if (!isReady) return null
if (isError) return <p>Invalid query string</p>
return (
<>
<pre>{JSON.stringify(data, null, 2)}</pre>
<button onClick={() => updateParams({ page: (data.page ?? 1) + 1 })}>
Next page
</button>
<button onClick={() => updateParams({ q: null })}>
Clear search
</button>
</>
)
}
```
### Returned object
| key | type | description |
| --- | --- | --- |
| `data` | **without resolver**: `Record<string, string \| undefined>`<br>**with resolver**: `{ [K in keyof Schema]?: Type }` | Parsed & coerced data. All schema keys exist but values may be `undefined`. |
| `isReady` | `boolean` | `true` after the first read of the URL. |
| `isError` | `boolean` | `true` if `safeParse` failed. |
| `updateParams` | `(params) => void` | Add / update / delete query keys. |
#### `updateParams()` value rules
| value | effect |
| --- | --- |
| `string` / `number` / `boolean` | Converted with `String()` and set in URL |
| `null` or `''` | Deletes the key |
| omit key | Leaves that key unchanged |
## Adapter example (Next.js)
```ts
import { useRouter, useSearchParams as nextSP } from 'next/navigation'
import type { QueryGuardAdapter } from 'use-query-guard'
export const nextAdapter = (): QueryGuardAdapter => {
const router = useRouter()
const current = nextSP()
return {
getSearch: () => '?' + current.toString(),
setSearch: qs => router.push(qs ? '?' + qs : '?'),
subscribe: cb => {
window.addEventListener('popstate', cb)
return () => window.removeEventListener('popstate', cb)
},
}
}
// Usage
const { data } = useQueryGuard({
resolver: schema,
adapter: nextAdapter(),
})
```
## API (excerpt)
```ts
function useQueryGuard<
S extends z.ZodObject<z.ZodRawShape> | undefined = undefined
>(options?: {
resolver?: S
preprocess?: (
key: S extends z.ZodObject<any> ? keyof z.infer<S> & string : string,
value: string
) => string
adapter?: QueryGuardAdapter
mode?: 'pick' | 'strict'
}): {
data: S extends z.ZodObject<any>
? { [K in keyof z.infer<S>]?: z.infer<S>[K] }
: Record<string, string | undefined>
isReady: boolean
isError: boolean
updateParams: (p: UpdateArgs<S>) => void
}
```
### オプション: `mode`
- `mode: 'pick'`(デフォルト): スキーマに一致するキーだけ部分的にパースし、1つでも不正値があれば `isError: true` になるが、他の値は取得できる
- `mode: 'strict'`: 全てのキーがスキーマに完全一致しないと `isError: true` となり、`data` も空になる
#### 例
```ts
// pickモード(デフォルト)
const { data, isError } = useQueryGuard({
resolver: schema,
mode: 'pick',
})
// strictモード
const { data, isError } = useQueryGuard({
resolver: schema,
mode: 'strict',
})
```
## License
MIT