このガイドでは、軽量・スキーマファースト・完全型安全な React 向フォームライブラリ Formisch を使ったフォーム構築を解説します。<Field /> コンポーネントでフォームを作成し、Valibot スキーマで検証し、エラー処理とアクセシビリティを実装する方法を学びます。
デモ#
以下のフォームを作成します。シンプルなテキスト入力とテキストエリアを持ちます。送信時にフォームデータを検証し、エラーを表示します。
注意: このデモでは、Formisch でのスキーマ検証とフォームエラーの動作を示すため、意図的にブラウザ検証を無効にしています。本番コードでは基本的なブラウザ検証を追加することを推奨します。
Component form-formisch-demo not found in registry.
アプローチ#
このフォームは、ヘッドレス・スキーマファーストなフォーム管理を実現する Formisch を活用しています。マークアップとスタイリングを完全にコントロールできる <Field /> コンポーネントを使ってフォームを構築します。
- フォーム状態管理に Formisch の
useFormフックを使用します。 - ネイティブの
<form>要素を送信処理でラップする<Form />コンポーネントを使用します。 - 制御された入力のために
<Field />レンダープロップコンポーネントを使用します。 - Valibot によるスキーマ検証を実装します。
- スキーマから推準される型安全なフィールドパス。
フォームメソッド#
Formisch はフォーム操作をフォームオブジェクトのメソッドではなく、トップレベル関数として公開しています。必要なものだけインポートしてください。
import { getInput, insert, reset, submit } from "@formisch/react"すべてのメソッドは同じシグネチャを持ちます。第一引数は常にフォームストア、第二引数(必要な場合)は常に設定オブジェクトです。
// フィールド値の取得
const email = getInput(form, { path: ["email"] })
// フォームを新しい初期値でリセット
reset(form, { initialInput: { email: "", password: "" } })
// 配列フィールド内のアイテムを移動
move(form, { path: ["items"], from: 0, to: 3 })この設計により API は柔軟性と一賫性を保持しています。状態の読み込み(getInput、getErrors)、書き込み(setInput、setErrors)、フォーム制御(submit、validate、focus)、配列操作(insert、remove、move、swap、replace)すべてに同じ (form, config) 形式が用いられます。詳細は full methods reference を参照してください。
アナトミー#
Formisch の <Field /> コンポーネントと Ketyap UI の <Field /> コンポーネントを使ったフォームの基本例。
<Form of={form} onSubmit={handleSubmit}>
<FieldGroup>
<FormischField of={form} path={["title"]}>
{(field) => (
<Field data-invalid={field.errors !== null}>
<FieldLabel htmlFor="form-title">Bug Title</FieldLabel>
<Input
{...field.props}
id="form-title"
value={field.input}
aria-invalid={field.errors !== null}
placeholder="Login button not working on mobile"
autoComplete="off"
/>
<FieldDescription>
Provide a concise title for your bug report.
</FieldDescription>
{field.errors && (
<FieldError errors={field.errors.map((message) => ({ message }))} />
)}
</Field>
)}
</FormischField>
</FieldGroup>
</Form>注意: Formisch は独自の Field コンポーネントを持っています。Ketyap UI の Field と名前が衝突するため、下記の例では Formisch 側を FormischField としてインポートし、Ketyap UI の Field は元の名前のまま使用しています。自分のコードではどちら側にもエイリアスを設定できます——一賫していればOKです。
フォーム#
フォームスキーマの作成#
Valibot スキーマでフォームの形式を定義します。Formisch はこのスキーマからインプット・アウトプット型を直接推準します。
import * as v from "valibot"
const FormSchema = v.object({
title: v.pipe(
v.string(),
v.minLength(5, "Bug title must be at least 5 characters."),
v.maxLength(32, "Bug title must be at most 32 characters.")
),
description: v.pipe(
v.string(),
v.minLength(20, "Description must be at least 20 characters."),
v.maxLength(100, "Description must be at most 100 characters.")
),
})フォームのセットアップ#
Formisch の useForm フックでフォームインスタンスを作成します。リゾルバーのステップは不要で、スキーマを useForm に直接渡します。
import { Form, Field as FormischField, useForm } from "@formisch/react"
import type { SubmitHandler } from "@formisch/react"
import * as v from "valibot"
const FormSchema = v.object({
title: v.pipe(
v.string(),
v.minLength(5, "Bug title must be at least 5 characters."),
v.maxLength(32, "Bug title must be at most 32 characters.")
),
description: v.pipe(
v.string(),
v.minLength(20, "Description must be at least 20 characters."),
v.maxLength(100, "Description must be at most 100 characters.")
),
})
export function BugReportForm() {
const form = useForm({
schema: FormSchema,
initialInput: {
title: "",
description: "",
},
})
const handleSubmit: SubmitHandler<typeof FormSchema> = (output) => {
// Do something with the validated form values.
console.log(output)
}
return (
<Form of={form} onSubmit={handleSubmit}>
{/* ... */}
{/* Build the form here */}
{/* ... */}
</Form>
)
}<Form /> コンポーネントはネイティブの <form> 要素をラップします。event.preventDefault() を呼び出し、検証を実行し、データが有効なときのみ onSubmit を呼び出します。受け取った output はスキーマから完全に型付けされます。
フォームの構築#
Formisch の <Field /> コンポーネントと Ketyap UI の <Field /> コンポーネントでフォームを作成できます。
完成#
以上です。クライアントサイド検証付きの完全なアクセシブルなフォームができました。
フォームを送信すると、検証済みデータで handleSubmit が呼び出されます。入力が無効な場合、Formisch が各無効フィールドの field.errors にエラーを入力し、UI に表示されます。
検証#
クライアントサイド検証#
Formisch は useForm に渡した Valibot スキーマでフォームデータを検証します。リゾルバーは不要——スキーマがランタイム検証と静的型の唯一のソースです。
import { useForm } from "@formisch/react"
const FormSchema = v.object({
title: v.string(),
description: v.optional(v.string()),
})
export function ExampleForm() {
const form = useForm({
schema: FormSchema,
initialInput: {
title: "",
description: "",
},
})
}検証モード#
Formisch は初回検証と再検証を分離しています。useForm の validate と revalidate オプションで設定します。
const form = useForm({
schema: FormSchema,
validate: "blur",
revalidate: "input",
})| オプション | 値 | 説明 |
|---|---|---|
validate | "submit" | 送信時に検証(デフォルト)。 |
validate | "blur" | フィールドがフォーカスを失ったときに検証。 |
validate | "input" | 入力変更ごとに検証。 |
validate | "initial" | フォーム作成時に即座に検証。 |
revalidate | "input" | 初回実行後、入力変更ごとに再検証(デフォルト)。 |
revalidate | "blur" | 初回実行後、blur 時に再検証。 |
revalidate | "submit" | 送信時のみ再検証。 |
エラーの表示#
<FieldError /> でフィールドの横にエラーを表示します。Formisch はエラーを文字列配列で返すので、<FieldError /> が期待する形式にマップしてください。スタイリングとアクセシビリティのため:
<Field />コンポーネントにdata-invalidを付けます。<Input />、<SelectTrigger />、<Checkbox />などのフォームコントロールにaria-invalidを付けます。
<FormischField of={form} path={["email"]}>
{(field) => (
<Field data-invalid={field.errors !== null}>
<FieldLabel htmlFor="form-email">Email</FieldLabel>
<Input
{...field.props}
id="form-email"
value={field.input}
type="email"
aria-invalid={field.errors !== null}
/>
{field.errors && (
<FieldError errors={field.errors.map((message) => ({ message }))} />
)}
</Field>
)}
</FormischField>フィールドタイプ別の使い方#
Formisch はフィールドと要素をバインドする2つの方法を提供します。
- ネイティブ HTML 要素(
<Input />、<Textarea />あど)——field.propsをスプレッドし、value={field.input}を提供します。name、ref、onChange、onBlur、onFocusは Formisch が自動で設定します。 - コンポーネントライブラリどの入力(Radix ベースの
<Select />、<Checkbox />、<RadioGroup />、<Switch />など)——field.inputから値を読み取り、field.onChange(value)を呼び出して更新します。
Input#
- 入力フィールドには
field.propsをスプレッドし、value={field.input}を提供します。 - エラー表示には
<Input />にaria-invalid、<Field />にdata-invalidを付けます。
Component form-formisch-input not found in registry.
<FormischField of={form} path={["username"]}>
{(field) => (
<Field data-invalid={field.errors !== null}>
<FieldLabel htmlFor="form-username">Username</FieldLabel>
<Input
{...field.props}
id="form-username"
value={field.input}
aria-invalid={field.errors !== null}
/>
{field.errors && (
<FieldError errors={field.errors.map((message) => ({ message }))} />
)}
</Field>
)}
</FormischField>Textarea#
- テキストエリアフィールドには
field.propsをスプレッドし、value={field.input}を提供します。 - エラー表示には
<Textarea />にaria-invalid、<Field />にdata-invalidを付けます。
Component form-formisch-textarea not found in registry.
<FormischField of={form} path={["about"]}>
{(field) => (
<Field data-invalid={field.errors !== null}>
<FieldLabel htmlFor="form-about">More about you</FieldLabel>
<Textarea
{...field.props}
id="form-about"
value={field.input}
aria-invalid={field.errors !== null}
placeholder="I'm a software engineer..."
className="min-h-[120px]"
/>
<FieldDescription>
Tell us more about yourself. This will be used to help us personalize
your experience.
</FieldDescription>
{field.errors && (
<FieldError errors={field.errors.map((message) => ({ message }))} />
)}
</Field>
)}
</FormischField>Select#
- Select コンポーネントには
field.inputを読み取り、<Select />のonValueChangeからfield.onChangeを呼び出します。 - エラー表示には
<SelectTrigger />にaria-invalid、<Field />にdata-invalidを付けます。
Component form-formisch-select not found in registry.
<FormischField of={form} path={["language"]}>
{(field) => (
<Field orientation="responsive" data-invalid={field.errors !== null}>
<FieldContent>
<FieldLabel htmlFor="form-language">Spoken Language</FieldLabel>
<FieldDescription>
For best results, select the language you speak.
</FieldDescription>
{field.errors && (
<FieldError errors={field.errors.map((message) => ({ message }))} />
)}
</FieldContent>
<Select value={field.input} onValueChange={field.onChange}>
<SelectTrigger
id="form-language"
aria-invalid={field.errors !== null}
className="min-w-[120px]"
>
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent position="item-aligned">
<SelectItem value="auto">Auto</SelectItem>
<SelectItem value="en">English</SelectItem>
</SelectContent>
</Select>
</Field>
)}
</FormischField>Checkbox#
- チェックボックス配列には
field.inputを読み取り、onCheckedChangeからfield.onChangeを呼び出して更新します。 - エラー表示には
<Checkbox />にaria-invalid、<Field />にdata-invalidを付けます。 - 適切なスタイリングと間距のため、
<FieldGroup />にdata-slot="checkbox-group"を付けてください。
Component form-formisch-checkbox not found in registry.
<FormischField of={form} path={["tasks"]}>
{(field) => (
<FieldSet>
<FieldLegend variant="label">Tasks</FieldLegend>
<FieldDescription>
Get notified when tasks you've created have updates.
</FieldDescription>
<FieldGroup data-slot="checkbox-group">
{tasks.map((task) => (
<Field
key={task.id}
orientation="horizontal"
data-invalid={field.errors !== null}
>
<Checkbox
id={`form-checkbox-${task.id}`}
aria-invalid={field.errors !== null}
checked={field.input?.includes(task.id) ?? false}
onCheckedChange={(checked) => {
const current = field.input ?? []
field.onChange(
checked === true
? [...current, task.id]
: current.filter((value) => value !== task.id)
)
}}
/>
<FieldLabel
htmlFor={`form-checkbox-${task.id}`}
className="font-normal"
>
{task.label}
</FieldLabel>
</Field>
))}
</FieldGroup>
{field.errors && (
<FieldError errors={field.errors.map((message) => ({ message }))} />
)}
</FieldSet>
)}
</FormischField>Radio Group#
- ラジオグループには
field.inputを読み取り、onValueChangeからfield.onChangeを呼び出します。 - エラー表示には
<RadioGroupItem />にaria-invalid、<Field />にdata-invalidを付けます。
Component form-formisch-radiogroup not found in registry.
<FormischField of={form} path={["plan"]}>
{(field) => (
<FieldSet>
<FieldLegend>Plan</FieldLegend>
<FieldDescription>
You can upgrade or downgrade your plan at any time.
</FieldDescription>
<RadioGroup value={field.input} onValueChange={field.onChange}>
{plans.map((plan) => (
<FieldLabel key={plan.id} htmlFor={`form-radiogroup-${plan.id}`}>
<Field
orientation="horizontal"
data-invalid={field.errors !== null}
>
<FieldContent>
<FieldTitle>{plan.title}</FieldTitle>
<FieldDescription>{plan.description}</FieldDescription>
</FieldContent>
<RadioGroupItem
value={plan.id}
id={`form-radiogroup-${plan.id}`}
aria-invalid={field.errors !== null}
/>
</Field>
</FieldLabel>
))}
</RadioGroup>
{field.errors && (
<FieldError errors={field.errors.map((message) => ({ message }))} />
)}
</FieldSet>
)}
</FormischField>Switch#
- スイッチには
field.inputを読み取り、onCheckedChangeからfield.onChangeを呼び出します。 - エラー表示には
<Switch />にaria-invalid、<Field />にdata-invalidを付けます。
Component form-formisch-switch not found in registry.
<FormischField of={form} path={["twoFactor"]}>
{(field) => (
<Field orientation="horizontal" data-invalid={field.errors !== null}>
<FieldContent>
<FieldLabel htmlFor="form-twoFactor">
Multi-factor authentication
</FieldLabel>
<FieldDescription>
Enable multi-factor authentication to secure your account.
</FieldDescription>
{field.errors && (
<FieldError errors={field.errors.map((message) => ({ message }))} />
)}
</FieldContent>
<Switch
id="form-twoFactor"
checked={field.input ?? false}
onCheckedChange={field.onChange}
aria-invalid={field.errors !== null}
/>
</Field>
)}
</FormischField>Complex Forms#
複数のフィールドと検証を持つ、より複雑なフォームの例。
Component form-formisch-complex not found in registry.
フォームのリセット#
Formisch はトップレベルの reset 関数を提供します。フォームストアを渡して初期入力にリセットします。
<Button type="button" variant="outline" onClick={() => reset(form)}>
Reset
</Button>新しい初期値でリセットしたり、ユーザーの入力内容を保持したままリセットすることもできます。
// 新しい初期値にリセット
reset(form, { initialInput: { title: "", description: "" } })
// サーバーデータに基準を同期し、ユーザーの編集内容を保持
reset(form, { initialInput: serverData, keepInput: true })配列フィールド#
Formisch は動的な配列フィールドを管理する <FieldArray /> コンポーネントとヘルパー関数群を提供します。アイテムの追加・削除・並び替えが必要なときに使用してください。
Component form-formisch-array not found in registry.
FieldArray の使い方#
<FieldArray /> は <Field /> と同じレンダープロップパターンを持ちます。items 配列にはアイテムごとに安定したキーが含まれ、React の key に使用するべきです。
import {
Field as FormischField,
FieldArray,
insert,
remove,
} from "@formisch/react"
export function ExampleForm() {
// ... form config
return (
<FieldArray of={form} path={["emails"]}>
{(fieldArray) => (
<FieldGroup className="gap-4">
{fieldArray.items.map((item, index) => (
<FormischField
key={item}
of={form}
path={["emails", index, "address"]}
>
{(field) => /* ... */}
</FormischField>
))}
</FieldGroup>
)}
</FieldArray>
)
}配列フィールドの構造#
<FieldSet /> に <FieldLegend /> と <FieldDescription /> を付けて配列フィールドを包みます。
<FieldSet className="gap-4">
<FieldLegend variant="label">Email Addresses</FieldLegend>
<FieldDescription>
Add up to 5 email addresses where we can contact you.
</FieldDescription>
<FieldGroup className="gap-4">{/* Array items go here */}</FieldGroup>
</FieldSet>アイテムの追加#
insert 関数で配列に新しいアイテムを追加します。デフォルトでは末尾に追加されます。at インデックスで特定位置に挿入することもできます。
<Button
type="button"
variant="outline"
size="sm"
onClick={() =>
insert(form, { path: ["emails"], initialInput: { address: "" } })
}
disabled={fieldArray.items.length >= 5}
>
Add Email Address
</Button>アイテムの削除#
remove 関数に at インデックスを渡して配列からアイテムを削除します。
import { remove } from "@formisch/react"
{
fieldArray.items.length > 1 && (
<InputGroupAddon align="inline-end">
<InputGroupButton
type="button"
variant="ghost"
size="icon-xs"
onClick={() => remove(form, { path: ["emails"], at: index })}
aria-label={`Remove email ${index + 1}`}
>
<XIcon />
</InputGroupButton>
</InputGroupAddon>
)
}Formisch はアイテムの並び替えや置換のために move、swap、replace も提供しています。いずれも同じ (form, config) シグネチャを持ちます。
配列の検証#
Valibot の array とパイプラインバリデータで配列フィールドを検証します。
const FormSchema = v.object({
emails: v.pipe(
v.array(
v.object({
address: v.pipe(
v.string(),
v.nonEmpty("Enter an email address."),
v.email("Enter a valid email address.")
),
})
),
v.minLength(1, "Add at least one email address."),
v.maxLength(5, "You can add up to 5 email addresses.")
),
})