検索や絞り込みのフォーム
検索や絞り込みのフォームは、送信した条件がURLに残るGETのフォームにすると、リンクを共有したりブラウザの戻るで前の条件に戻ったりできます。@k8ordo/stateと組み合わせると、フォームの制約とURLの状態を1つのスキーマで書けます。
このページの内容
URLの状態とスキーマを共有する
@k8ordo/stateのdefinePageStateで、URLに置く状態のurlスキーマを書きます。同じスキーマをformFieldsに渡せば、フォームの制約もそこから作られます。
list-state.tsexport const listState = definePageState('product-list', {
url: z.object({
q: z.string().default(''),
inStock: z.stringbool().default(false),
}),
});page.tsxconst filterFields = formFields(listState.url);
export default function ProductsPage() {
return <Filters fields={filterFields} />;
}メモ
このスキーマは、useAppStateがブラウザで値を読むときにも使うので、クライアントのバンドルに入ります。zod/miniで書くとバンドルを小さくできます。
stateを渡さずにuseFormを呼ぶ
送信を受け取るServer Actionが無いので、useFormには入力欄の情報だけを渡します。入力欄の初期値には、useAppStateで読んだいまの状態をdefaultValueで渡します。
filters.tsxconst form = useForm(fields);
const [current] = useAppState(listState);
const q = form.field('q');
const inStock = form.field('inStock');
<form {...form.props} method="get">
<input {...q.input} defaultValue={current.q} type="search" />
<input {...inStock.input} defaultChecked={current.inStock} />
<button type="submit">Filter</button>
</form>stateが無くても、送信のときの検証は行います。スキーマに合わない条件は、URLに書き込まれる前に止まります。
メモ
入力欄の値はDOMが持つので、ブラウザの戻るでURLが変わっても、入力欄の値は変わりません。入力欄もURLの条件に合わせたいときは、<form key={search}>のようにURLが変わるたびにフォームを作り直します。
絞り込みのフォームを試す
@k8ordo/formのランディングと同じデモです。送信すると、このページのURLが書き換わります。
- URL
クエリなし(すべて既定値)- state
{"q":"","min":0,"inStock":false}
試してみる
- キーワードを入れて「絞り込む」を押すと、URLに
?q=が付きます。ページは読み込み直されません。 - 「在庫ありのみ」にチェックして送ると、URLに
inStock=trueが付きます。 - ブラウザの戻るを押すと、URLとstateの行が前の条件に戻ります。入力欄の値はそのまま残ります。
送信はページの読み込みにならない
@k8ordo/routerの下では、同じパスへのGETのフォームの送信をルーターが受け取り、ページの読み込みではなく状態の更新として扱います。スクロールの位置もフォーカスもそのまま保たれます。
JavaScriptが無いときは、ふつうのGETのフォームとして送信され、同じURLにたどり着きます。
真偽値はz.stringbool()で書く
URLに置く真偽値は、z.stringbool()で書きます。チェックボックスはupdate()が書くのと同じtrueの綴りを送るので、フォームの送信でできるURLと、update()が書くURLが一致します。
落とし穴
@k8ordo/uiのCheckboxには、展開したvalueが届きません。z.stringbool()の欄は、素の<input>にinputを展開して描いてください。
サーバーで条件を読む
@k8ordo/serverでは、ページでsearchとしてURLのスキーマを書き出すと、検証済みの条件を受け取って描画できます。JavaScriptが届く前から、検索結果を含んだページを返せます。
routes/products/page.tsxexport const search = listState.url;
export default async function ProductsPage({
search,
}: PageProps<'/products'>) {
const products = await findProducts(search);
return <ProductList products={products} />;
}@k8ordo/staticのようにサーバーで条件を読めない場合は、サーバーの描画には既定値が使われ、ハイドレーションのあとで送信した条件が反映されます。