- Accordion
- Alert
- Alert Dialog
- Aspect Ratio
- Attachment
- Avatar
- Badge
- Breadcrumb
- Bubble
- Button
- Button Group
- Calendar
- Card
- Carousel
- Chart
- Checkbox
- Collapsible
- Combobox
- Command
- Context Menu
- Data Table
- Date Picker
- Dialog
- Direction
- Drawer
- Dropdown Menu
- Empty
- Field
- Hover Card
- Input
- Input Group
- Input OTP
- Item
- Kbd
- Label
- Marker
- Menubar
- Message
- Message Scroller
- Native Select
- Navigation Menu
- Pagination
- Popover
- Progress
- Questionnaire
- Radio Group
- Resizable
- Scroll Area
- Select
- Separator
- Sheet
- Sidebar
- Skeleton
- Slider
- Spinner
- Switch
- Table
- Tabs
- Textarea
- Toast
- Toggle
- Toggle Group
- Tooltip
- Typography
このガイドでは、軽量でスキーマファースト、完全に型安全な React 用フォーム ライブラリ Formisch を使ってフォームを構築します。<Field /> コンポーネントでフォームを作成し、Valibot スキーマで検証し、エラーを処理し、アクセシビリティを確保します。
デモ#
次のフォームを構築します。単純なテキスト入力とテキストエリアを備えています。送信時にフォーム データを検証し、エラーを表示します。
注: このデモでは、スキーマ検証とフォーム エラーが Formisch でどのように動作するかを示すため、意図的にブラウザー検証を無効にしています。本番コードでは基本的なブラウザー検証を追加することを推奨します。
コンポーネント form-formisch-demo が見つかりません。
アプローチ#
このフォームでは、スタイルを持たないスキーマファーストのフォーム処理に Formisch を利用します。マークアップとスタイルを完全に柔軟に制御できる <Field /> コンポーネントでフォームを構築します。
- フォーム状態の管理に Formisch の
useFormフックを使います。 - ネイティブの
<form>要素を送信処理付きでラップする<Form />コンポーネントを使います。 - 制御された入力に
<Field />の render prop コンポーネントを使います。 - Valibot でスキーマ検証を行います。
- スキーマから型安全なフィールド パスを推論します。
フォーム メソッド#
Formisch はフォーム オブジェクトのメソッドではなく、トップレベル関数としてフォーム操作を公開します。必要なものだけをインポートします。
import { getInput, insert, reset, submit } from "@formisch/react"すべてのメソッドは同じシグネチャに従います。最初のパラメーターは常にフォーム ストアで、2 番目のパラメーター(必要な場合)は常に設定オブジェクトです。
// Read a field value
const email = getInput(form, { path: ["email"] })
// Reset the form with new initial values
reset(form, { initialInput: { email: "", password: "" } })
// Move an item in a field array
move(form, { path: ["items"], from: 0, to: 3 })この設計により、すべてのメソッドで API の柔軟性と一貫性が保たれます。このガイド全体で、状態の読み取り(getInput、getErrors)、状態の書き込み(setInput、setErrors)、フォーム制御(submit、validate、focus)、配列操作(insert、remove、move、swap、replace)に同じ (form, config) 形式を使います。詳しくはメソッドの完全なリファレンスを参照してください。
構成#
Formisch の <Field /> コンポーネントと shadcn の <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 コンポーネントがあります。shadcn の Field との名前の衝突を避けるため、以下の例では Formisch 版を FormischField としてインポートし、shadcn の Field は元の名前を維持しています。自分のコードではどちら側にも別名を付けられますが、一貫して使用してください。
使い方#
フォーム スキーマを作成する#
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 に直接渡すため、resolver の手順はありません。
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 /> コンポーネントと shadcn の <Field /> コンポーネントを使ってフォームを構築できます。
完了#
以上で、クライアント側検証を備えた完全にアクセシブルなフォームが完成しました。
フォームを送信すると、検証済みのフォーム データを引数に handleSubmit 関数が呼び出されます。フォーム データが無効な場合、Formisch は無効な各フィールドの field.errors を設定し、UI に表示します。
バリデーション#
クライアント側検証#
Formisch は useForm に渡した Valibot スキーマを使ってフォーム データを検証します。resolver はありません。実行時検証と静的型の両方で、スキーマが唯一の信頼できる情報源になります。
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" | 初回検証後、フォーカスを失ったときに再検証します。 |
revalidate | "submit" | フォーム送信時だけ再検証します。 |
エラーの表示#
<FieldError /> を使ってフィールドの隣にエラーを表示します。Formisch はエラーを文字列配列として返すため、<FieldError /> が受け取る形にマッピングします。スタイルとアクセシビリティのために次を行います。
<Field />コンポーネントにdata-invalidprop を追加します。<Input />、<SelectTrigger />、<Checkbox />などのフォーム コントロールにaria-invalidprop を追加します。
<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}を渡します。Formisch がname、ref、onChange、onBlur、onFocusを設定します。 - コンポーネント ライブラリの入力(Radix ベースの
<Select />、<Checkbox />、<RadioGroup />、<Switch />など)—field.inputから値を読み取り、field.onChange(value)を呼び出して更新します。
入力#
- 入力フィールドでは
field.propsを展開し、value={field.input}を渡します。 - エラーを表示するには、
<Input />コンポーネントにaria-invalidprop、<Field />コンポーネントにdata-invalidprop を追加します。
コンポーネント form-formisch-input が見つかりません。
<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>テキストエリア#
- テキストエリア フィールドでは
field.propsを展開し、value={field.input}を渡します。 - エラーを表示するには、
<Textarea />コンポーネントにaria-invalidprop、<Field />コンポーネントにdata-invalidprop を追加します。
コンポーネント form-formisch-textarea が見つかりません。
<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 コンポーネントでは
field.inputを読み取り、<Select />のonValueChangeからfield.onChangeを呼び出します。 - エラーを表示するには、
<SelectTrigger />コンポーネントにaria-invalidprop、<Field />コンポーネントにdata-invalidprop を追加します。
コンポーネント form-formisch-select が見つかりません。
<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>チェックボックス#
- チェックボックス配列では
field.inputを読み取り、field.onChangeを使ってonCheckedChangeから更新します。 - エラーを表示するには、
<Checkbox />コンポーネントにaria-invalidprop、<Field />コンポーネントにdata-invalidprop を追加します。 - 適切なスタイルと間隔のため、
<FieldGroup />コンポーネントにdata-slot="checkbox-group"を追加してください。
コンポーネント form-formisch-checkbox が見つかりません。
<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>ラジオ グループ#
- ラジオ グループでは
field.inputを読み取り、onValueChangeからfield.onChangeを呼び出します。 - エラーを表示するには、
<RadioGroupItem />コンポーネントにaria-invalidprop、<Field />コンポーネントにdata-invalidprop を追加します。
コンポーネント form-formisch-radiogroup が見つかりません。
<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>スイッチ#
- スイッチでは
field.inputを読み取り、onCheckedChangeからfield.onChangeを呼び出します。 - エラーを表示するには、
<Switch />コンポーネントにaria-invalidprop、<Field />コンポーネントにdata-invalidprop を追加します。
コンポーネント form-formisch-switch が見つかりません。
<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>複雑なフォーム#
複数のフィールドと検証を含む、より複雑なフォームの例です。
コンポーネント form-formisch-complex が見つかりません。
フォームをリセットする#
Formisch はトップレベルの reset 関数を公開します。フォーム ストアを渡すと初期入力へリセットできます。
<Button type="button" variant="outline" onClick={() => reset(form)}>
Reset
</Button>新しい初期値へリセットしたり、ユーザーの現在の入力を保持したままリセットしたりすることもできます。
// Reset to a fresh set of initial values
reset(form, { initialInput: { title: "", description: "" } })
// Sync the baseline to new server data, but keep the user's edits
reset(form, { initialInput: serverData, keepInput: true })配列フィールド#
Formisch は動的な配列フィールドを管理するための <FieldArray /> コンポーネントとヘルパー関数群を提供します。項目の追加、削除、並べ替えが必要な場合に使用します。
コンポーネント form-formisch-array が見つかりません。
FieldArray を使う#
<FieldArray /> は <Field /> と同じ render prop パターンに従います。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>
)
}配列フィールドの構造#
配列フィールドを <FieldLegend /> と <FieldDescription /> を持つ <FieldSet /> でラップします。
<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>項目を削除する#
at インデックスを指定した remove 関数を使って配列から項目を削除します。
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.")
),
})