Introduction
URL クエリを型安全に扱うための、依存ゼロコア + 追加パッケージ構成。
What is query-guard?
query-guard は URL クエリ(search params)をアプリの状態として扱うための TypeScript ライブラリです。URLSearchParams を基盤にしつつ、 Resolver によって型を保証します。
Design goals
- dependency-freeコアは依存ゼロ。必要に応じて拡張。
- standardsURLSearchParams など Web 標準に準拠。
- TS-friendly型推論と明確な API で事故を減らす。
- adapter-based環境に合わせた Adapter で拡張可能。
Non-goals
- router-specific特定ルーターへの深い統合は行いません。
- implicit coercion型変換は Resolver に委譲します。
- nested params
{ a: { b: 1 } }のような深い構造は対象外。
Features
シンプルなコアと、必要に応じて追加できる拡張。
Compatibility
Quick Start
Adapter と Resolver を組み合わせて、URL クエリを型安全な状態に。
Install Core
まずコアパッケージを追加します。
$ pnpm add @liha-labs/query-guardMinimal Usage
Adapter と Resolver を渡して guard を作成します。
import { createBrowserAdapter, createQueryGuard } from '@liha-labs/query-guard'
const resolver = {
resolve: ({ raw }) => ({ value: { q: raw.q ?? '' } }),
serialize: (value) => ({ q: value.q })
}
const guard = createQueryGuard({
adapter: createBrowserAdapter(),
resolver,
defaultValue: { q: '' }
})
guard.set({ q: 'hello' })Typed Resolver
型付き Resolver で raw ↔ typed を明確化します。
type Queries = {
page: number
q: string
}
const resolver = {
resolve: ({ raw }) => ({
value: {
page: Number(raw.page ?? 1),
q: raw.q ?? ''
}
}),
serialize: (value: Queries) => ({
page: String(value.page),
q: value.q
})
}unknownPolicy
未知のキーを保持するか、落とすかを選択できます。
const guard = createQueryGuard({
adapter: createBrowserAdapter(),
resolver,
defaultValue: { page: 1 },
unknownPolicy: 'drop'
})
// URL上の未知キーを落として保存
guard.set({ page: 2 })React Integration
React では @liha-labs/query-guard-react を追加し、Provider + Hook で使います。
$ pnpm add @liha-labs/query-guard-react reactimport { QueryGuardProvider, useQueryGuard } from '@liha-labs/query-guard-react'
import { createBrowserAdapter } from '@liha-labs/query-guard'
const adapter = createBrowserAdapter()
const resolver = {
resolve: ({ raw }) => ({ value: { page: Number(raw.page ?? 1) } }),
serialize: (value) => ({ page: String(value.page) })
}
function App() {
return (
<QueryGuardProvider adapter={adapter} resolver={resolver} defaultValue={{ page: 1 }}>
<Pager />
</QueryGuardProvider>
)
}
function Pager() {
const { queries, set } = useQueryGuard<{ page: number }>()
return <button onClick={() => set({ page: queries.page + 1 })}>Next</button>
}Zod Resolver
検証や型変換を入れる場合は @liha-labs/query-guard-resolvers を利用します。
$ pnpm add @liha-labs/query-guard-resolvers zodimport { z } from 'zod'
import { createBrowserAdapter, createQueryGuard } from '@liha-labs/query-guard'
import { zodResolver } from '@liha-labs/query-guard-resolvers'
const schema = z.object({
page: z.coerce.number().int().min(1).default(1),
q: z.string().default('')
})
const resolver = zodResolver(schema)
const guard = createQueryGuard({
adapter: createBrowserAdapter(),
resolver,
defaultValue: { page: 1, q: '' }
})Usage
実務の「困った」を解決する、逆引きリファレンス。
Create a guard
createQueryGuard に Adapter と Resolver を渡して作成します。
import { createBrowserAdapter, createQueryGuard } from '@liha-labs/query-guard'
const resolver = {
resolve: ({ raw }) => ({ value: { q: raw.q ?? '' } }),
serialize: (value) => ({ q: value.q })
}
const guard = createQueryGuard({
adapter: createBrowserAdapter(),
resolver,
defaultValue: { q: '' }
})Update & Delete
set の undefined は削除として扱われます。
// setQueries: 完全な置き換え(serialize結果)
guard.setQueries({ q: 'hello' })
// set: patch(undefinedは削除)
guard.set({ q: undefined }) // URLから削除Reset
clear は管轄キーを削除、write-defaults は既定値をURLへ書き込みます。
// clear: 管轄キーを削除
guard.reset({ mode: 'clear' })
// write-defaults: defaultValue を書き込み
guard.reset({ mode: 'write-defaults' })History & unknownPolicy
History 更新方法と未知キーの扱いを制御できます。
const guard = createQueryGuard({
adapter: createBrowserAdapter(),
resolver,
defaultValue: { page: 1 },
history: 'push',
unknownPolicy: 'drop'
})React Hook
実運用では Provider に共通設定を集約し、画面ごとの差分だけ hook 側で上書きするのが分かりやすいです。
import { QueryGuardProvider, useQueryGuard } from '@liha-labs/query-guard-react'
import { createBrowserAdapter } from '@liha-labs/query-guard'
const adapter = createBrowserAdapter()
const resolver = {
resolve: ({ raw }) => ({
value: { page: Number(raw.page ?? 1), q: String(raw.q ?? '') }
}),
serialize: (value) => ({
page: String(value.page),
q: value.q
})
}
function App() {
return (
<QueryGuardProvider
adapter={adapter}
resolver={resolver}
defaultValue={{ page: 1, q: '' }}
history="replace"
unknownPolicy="keep"
>
<Pager />
</QueryGuardProvider>
)
}
function Pager() {
// Provider から resolver/defaultValue を引き継ぐ
const { queries, set } = useQueryGuard<{ page: number; q: string }>()
return <button onClick={() => set({ page: queries.page + 1 })}>Next</button>
}React: Hook options で部分上書き
Provider の設定をベースにしつつ、1画面だけ history を変えたい場合の例です。
import { useQueryGuard } from '@liha-labs/query-guard-react'
function SearchPage() {
const { queries, set } = useQueryGuard<{ page: number; q: string }>({
history: 'push' // Provider の history をこの hook だけ上書き
})
const onSubmit = (nextQ: string) => set({ q: nextQ, page: 1 })
return <button onClick={() => onSubmit('react')}>Search</button>
}React: options なしで使う場合
何も渡さない場合は fallback が使われます。プロトタイピング向けで、実運用では Resolver の明示を推奨します。
function Prototype() {
const { queries, set } = useQueryGuard()
// fallback resolver: raw 値をそのまま扱う
// fallback defaultValue: {}
return <button onClick={() => set({ page: '2' })}>{String(queries.page ?? '1')}</button>
}Zod Resolver
型変換は Zod 側に任せる構成です(z.coerce を利用)。
import { z } from 'zod'
import { zodResolver } from '@liha-labs/query-guard-resolvers'
const resolver = zodResolver(
z.object({ page: z.coerce.number().int().min(1).default(1) })
)API Reference
query-guard の主要 API と基本仕様。
createQueryGuard(options)
URL クエリの状態管理を行う QueryGuard を生成します。
createQueryGuard<T>(options: QueryGuardOptions<T>): QueryGuard<T>createBrowserAdapter()
ブラウザ用 Adapter。popstate を購読し、adapter 経由の setSearch 時にも通知します。
createBrowserAdapter(): QueryGuardAdapterQueryGuardOptions
QueryGuardAdapter検索文字列の読み書きと変更通知を提供します。
QueryResolver<T>raw ↔ typed 変換の責務を持ちます。
T初期値。reset の write-defaults でも利用されます。
'keep' | 'drop'未知キーの保持/除外を制御します。
'replace' | 'push'History API の更新方法を指定します。
QueryGuard API
getSearch()getRaw()getQueries()getMeta()setQueries(next, options?)set(patch, options?)reset(options?)subscribe(listener)set()のundefinedは削除扱い。reset()はclear/write-defaultsを選択。
QueryResolver / QueryRaw
Resolver は raw → typed / typed → raw を定義します。
Record<string, string | string[]>URLSearchParams から得られる raw データ形式です。
React API
useQueryGuard<T>(options?: UseQueryGuardOptions<T>)adapter / resolver / defaultValue / history / unknownPolicyを全 hook に配布します。
hook options → Provider → fallback の順で解決されます。
adapter は browser なら createBrowserAdapter()。resolver は passthrough、 defaultValue は {}。defaultValue が無い状態で unknownPolicy: 'drop' が要求された場合は 'keep' にフォールバックします。
non-browser 環境で adapter が未指定の場合、useQueryGuard はエラーを投げます。
Examples
最小構成のまま、実務に合わせて拡張していく例。
Provider + Hook
Adapter / policy を Provider で共有し、hook 側は最小オプションに。
import { QueryGuardProvider, useQueryGuard } from '@liha-labs/query-guard-react'
import { createBrowserAdapter } from '@liha-labs/query-guard'
const adapter = createBrowserAdapter()
const resolver = {
resolve: ({ raw }) => ({
value: { page: Number(raw.page ?? 1), q: String(raw.q ?? '') }
}),
serialize: (value) => ({
page: String(value.page),
q: value.q
})
}
function App() {
return (
<QueryGuardProvider
adapter={adapter}
resolver={resolver}
defaultValue={{ page: 1, q: '' }}
history="replace"
unknownPolicy="keep"
>
<Pager />
</QueryGuardProvider>
)
}
function Pager() {
const { queries, set } = useQueryGuard<{ page: number; q: string }>()
return <button onClick={() => set({ page: queries.page + 1 })}>Next</button>
}Hook Override
Provider をベースにしつつ、一部オプションだけ hook 側で上書きできます。
import { useQueryGuard } from '@liha-labs/query-guard-react'
function SearchPage() {
const { queries, set } = useQueryGuard<{ page: number; q: string }>({
history: 'push'
})
const goNext = () => set({ page: queries.page + 1 })
return <button onClick={goNext}>Next</button>
}unknownPolicy
URL に混在する未知キーを保持するか、落とすかを選べます。
import { createBrowserAdapter, createQueryGuard } from '@liha-labs/query-guard'
const resolver = {
resolve: ({ raw }) => ({ value: { page: Number(raw.page ?? 1) } }),
serialize: (value) => ({ page: String(value.page) })
}
const guard = createQueryGuard({
adapter: createBrowserAdapter(),
resolver,
defaultValue: { page: 1 },
unknownPolicy: 'drop'
})
// URL から未知キーを除去して保存
guard.set({ page: 2 })Zod Resolver
型変換は Zod に任せる構成。z.coerce を使います。
import { z } from 'zod'
import { zodResolver } from '@liha-labs/query-guard-resolvers'
const resolver = zodResolver(
z.object({ page: z.coerce.number().int().min(1).default(1) })
)SSR Adapter
非ブラウザ環境では Adapter を自作して注入します。
import { QueryGuardProvider, useQueryGuard } from '@liha-labs/query-guard-react'
const adapter = {
getSearch: () => '?page=1',
setSearch: () => {},
subscribe: () => () => {}
}
const resolver = {
resolve: ({ raw }) => ({ value: { page: Number(raw.page ?? 1) } }),
serialize: (value) => ({ page: String(value.page) })
}
function Page() {
return (
<QueryGuardProvider adapter={adapter} resolver={resolver} defaultValue={{ page: 1 }}>
<Inner />
</QueryGuardProvider>
)
}
function Inner() {
const { queries } = useQueryGuard<{ page: number }>()
return <p>{queries.page}</p>
}