0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Standard Schema対応のURLクエリパラメータ検証Reactフックを作った話

0
Last updated at Posted at 2026-10-31

TL;DR

  • URLのクエリパラメータを、Zod / Valibot / ArkType など Standard Schema 対応のバリデーションライブラリ で検証できる React フックを作りました
  • スキーマは フィールドごと に渡すので、1つの壊れたパラメータが他のパラメータを巻き込みません
  • クライアント専用(SPA / クライアントコンポーネント向け)で、その制約は隠さず明記しています

こんな人向けの記事です

  • ReactでURLのクエリパラメータ(?page=2&q=hello)を扱っていて、型安全にバリデーションしたい
  • ZodやValibotを使っているが、ライブラリごとに連携コードを書き分けるのが面倒
  • 「Standard Schema」という言葉を最近見かけたが、何が嬉しいのかよく分からない

はじめに

URLのクエリパラメータをReactで扱うとき、こんなコードを書いたことはないでしょうか。

const searchParams = new URLSearchParams(window.location.search);
const page = Number(searchParams.get('page')) || 1;
const q = searchParams.get('q') || '';

シンプルですが、コンポーネントが増えるたびに書き続けるのはつらいですし、pageにabcのような不正な値が入っていたときのフォールバックも、書く人によってバラバラになりがちです。

Zodなどのバリデーションライブラリと組み合わせてURLパラメータを型安全に扱うライブラリは既にいくつもあります。今回作った @standard-search-params/react も同じ課題に取り組んでいますが、設計の軸に Standard Schema という仕様を据えているのが特徴です。

npm install @standard-search-params/react

Standard Schemaとは

Standard Schema は、Zod・Valibot・ArkTypeなど、異なるバリデーションライブラリが共通のインターフェースでやり取りできるようにする仕様です。バリデータが ~standard というプロパティ経由で共通の validate() メソッドを持つ、というだけの非常にミニマルな取り決めです。

何が嬉しいかというと、ライブラリ作者が「Zod専用」「Valibot専用」と作り分ける必要がなくなる点です。Standard Schemaに対応したツールを1つ作れば、利用者がZodを使っていてもValibotを使っていても、同じように動きます。

@standard-search-params/react は、Standard Schemaさえ満たしていればどのバリデーションライブラリとも組み合わせられます。

import { z } from 'zod';
import { useStandardSearchParams } from '@standard-search-params/react';

const schema = {
  page: z.coerce.number().int().min(1),
  q: z.string().min(1),
};

function SearchResults() {
  const { validatedSearchParams, isSearchParamsReady } =
    useStandardSearchParams(schema);

  if (!isSearchParamsReady) return null;

  return <div>page: {validatedSearchParams.page ?? 1}</div>;
}

Valibotを使いたい場合は、z.coerce.number() を v.pipe(v.string(), v.transform(Number)) のように書き換えるだけで、フック側のコードは変わりません。ZodとValibotを同じschemaオブジェクトの中で混在させることもできます。

import { z } from 'zod';
import * as v from 'valibot';

const schema = {
  page: z.coerce.number().int(),
  q: v.pipe(v.string(), v.minLength(1)),
};

なぜ「1つのobjectスキーマ」ではなく「フィールドごとのスキーマ」なのか

URLパラメータ全体を1つの大きなobjectスキーマで検証する方式には弱点があります。どこか1つのフィールドが不正だと、全体が丸ごと無効になってしまうことです。

?page=2&q=hello&sort=bad

このURLで sort が想定外の値だったとき、object全体を1つのスキーマで検証していると、正しい page や q まで巻き添えで失われるケースがあります。

@standard-search-params/react はこれを避けるため、キーごとに独立したスキーマを渡す設計にしています。

searchParams          // { page: '2', q: 'hello', sort: 'bad' }  (生の文字列)
validatedSearchParams // { page: 2, q: 'hello' }                  (パース済み、sortは除外)

sort だけが validatedSearchParams から除外され、page と q は問題なく使えます。古いリンクやユーザーが手で書き換えたURLを扱うプロダクトでは、地味に効いてくる性質だと思っています。

クライアント専用という割り切り

このフックは window.location.search を読むため、サーバーサイドレンダリング中は動きません。これは意図的な設計です。

SSRフレームワーク(Next.jsなど)の中で使ってもクラッシュやhydrationの不整合は起きません。isSearchParamsReady はサーバーレンダリング中と最初のクライアントレンダリング中は false のままで、クライアントでURLを読み終えた瞬間に true になります。ただし、その切り替わりまでの一瞬「まだロードされていない」状態が生じることは避けられません。

サーバーレンダリングの結果にバリデーション済みのパラメータを含めたい場合(例: Next.jsのServer Componentが受け取る searchParams prop)は、このフックではなく、そのオブジェクトを直接スキーマで検証してください。このフックは、クライアントレンダリングのアプリやクライアントコンポーネントでの利用を想定しています。

ブラウザの戻る/進むへの対応

v0.2.0で、URLの変化に追従する2つの仕組みを追加しました。

ブラウザの戻る/進むボタンには、listenToPopstate オプションで対応します。

const { validatedSearchParams } = useStandardSearchParams(schema, {
  listenToPopstate: true,
});

デフォルトはOFFです。「URLはmount時に1回だけ読む」というこのライブラリの基本の挙動を、既存の利用者に対して黙って変えないための判断です。自動で再検証が走る挙動は、明示的にオプトインしてもらう形にしています。

SPAルーターによるページ遷移(router.push() や navigate())は popstate イベントを発火しないため、別途 refresh() を公開しています。

// Next.js の例
const { validatedSearchParams, refresh } = useStandardSearchParams(schema);
const pathname = usePathname();
const searchParams = useSearchParams();

useEffect(() => {
  refresh();
}, [pathname, searchParams, refresh]);

開発時に気づける小さな仕掛け

このフックはURLを mount時に1回だけ 読みます。そのためschemaをインラインで書いても問題ありません。一方で、schemaのキー構成そのものが実行時に変わる場合(たとえば管理者ユーザーだけ追加のパラメータを検証する場合)は落とし穴になります。

これに対しては、開発モード限定で console.warn が出るようにしています。

useStandardSearchParams: schema keys changed after mount
(was [page], now [page,q]), but this hook only reads the URL once,
on mount, so the new keys won't be read. If the set of keys genuinely
needs to change at runtime, remount the component (e.g. with a `key`
prop) — memoizing the schema object does not cause a re-parse.

メッセージの通り、対処法は useMemo ではなく key propによる強制リマウントです。useMemo でschemaの参照を安定させても再パースは起きません。「直ったように見えて何も直っていない」対処法に誘導しないよう、警告メッセージ自体に正しい対処法を書いています。

まとめ

@standard-search-params/react は、次の点を軸にした小さなライブラリです。

  • Standard Schema対応で、Zod / Valibot / ArkType のどれとでも組み合わせられる
  • フィールドごとの独立したバリデーションで、1つの壊れたパラメータが他を巻き込まない
  • クライアント専用という制約を隠さず明記している
  • ブラウザバック対応とSPAルーター連携は、どちらも「安全なデフォルト+明示的なオプトイン」で提供する

機能を積み増すより、挙動が予測できることを優先して作っています。フィードバックやIssueはいつでも歓迎です。


※ Zenn にも同じ内容を投稿しています: https://zenn.dev/wdp/articles/72729dd75e506b

続編: URLパラメータ用Reactフックの設計で、一度決めたことを見直した3つの話

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?