このガイドでは、TanStack Form を使ったフォーム構築を解説します。<Field /> コンポーネントでフォームを作成し、Zod でスキーマ検証を行い、エラー処理とアクセシビリティを実装する方法を学べます。
デモ#
以下のフォームを作成します。シンプルなテキスト入力とテキストエリアを持ちます。送信時にフォームデータを検証し、エラーを表示します。
注意: このデモでは、TanStack Form でのスキーマ検証とフォームエラーの動作を示すため、意図的にブラウザ検証を無効にしています。本番コードでは基本的なブラウザ検証を追加することを推奨します。
Component form-tanstack-demo not found in registry.
アプローチ#
このフォームは、強力でヘッドレスなフォーム管理を実現する TanStack Form を活用しています。マークアップとスタイリングを完全にコントロールできる <Field /> コンポーネントを使ってフォームを構築します。
- フォーム状態管理に TanStack Form の
useFormフック を使用します。 - 制御された入力のために
form.Fieldコンポーネントのレンダープロップパターンを使用します。 - アクセシブルなフォーム構築のために
<Field />コンポーネントを使用します。 - Zod によるクライアントサイド検証を実装します。
- リアルタイムな検証フィードバックを提供します。
アナトミー#
<Field /> コンポーネントと TanStack Form を使ったフォームの基本的な例。
<form
onSubmit={(e) => {
e.preventDefault()
form.handleSubmit()
}}
>
<FieldGroup>
<form.Field
name="title"
children={(field) => {
const isInvalid =
field.state.meta.isTouched && !field.state.meta.isValid
return (
<Field data-invalid={isInvalid}>
<FieldLabel htmlFor={field.name}>Bug Title</FieldLabel>
<Input
id={field.name}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
aria-invalid={isInvalid}
placeholder="Login button not working on mobile"
autoComplete="off"
/>
<FieldDescription>
Provide a concise title for your bug report.
</FieldDescription>
{isInvalid && <FieldError errors={field.state.meta.errors} />}
</Field>
)
}}
/>
</FieldGroup>
<Button type="submit">Submit</Button>
</form>フォーム#
スキーマの作成#
Zod スキーマでフォームの形式を定義します。
注意: この例では zod v3 を使用しています。TanStack Form は validators API を通じて Zod および他の Standard Schema 対応ライブラリとシームレスに連携します。
import * as z from "zod"
const formSchema = z.object({
title: z
.string()
.min(5, "Bug title must be at least 5 characters.")
.max(32, "Bug title must be at most 32 characters."),
description: z
.string()
.min(20, "Description must be at least 20 characters.")
.max(100, "Description must be at most 100 characters."),
})フォームのセットアップ#
TanStack Form の useForm フックで Zod 検証付きのフォームインスタンスを作成します。
import { useForm } from "@tanstack/react-form"
import { toast } from "sonner"
import * as z from "zod"
const formSchema = z.object({
// ...
})
export function BugReportForm() {
const form = useForm({
defaultValues: {
title: "",
description: "",
},
validators: {
onSubmit: formSchema,
},
onSubmit: async ({ value }) => {
toast.success("Form submitted successfully")
},
})
return (
<form
onSubmit={(e) => {
e.preventDefault()
form.handleSubmit()
}}
>
{/* ... */}
</form>
)
}この例では onSubmit でフォームデータを検証しています。TanStack Form は他の検証モードもサポートしています。詳細は documentation を参照してください。
フォームの構築#
TanStack Form の form.Field コンポーネントと <Field /> コンポーネントでフォームを作成できます。
完成#
以上です。クライアントサイド検証付きの完全なアクセシブルなフォームができました。
フォームを送信すると、検証済みデータで onSubmit が呼び出されます。入力が無効な場合、TanStack Form が各フィールドの横にエラーを表示します。
検証#
クライアントサイド検証#
TanStack Form は Zod スキーマでフォームデータを検証します。ユーザーが入力するたびにリアルタイムで検証が行われます。
import { useForm } from "@tanstack/react-form"
const formSchema = z.object({
// ...
})
export function BugReportForm() {
const form = useForm({
defaultValues: {
title: "",
description: "",
},
validators: {
onSubmit: formSchema,
},
onSubmit: async ({ value }) => {
console.log(value)
},
})
return <form onSubmit={/* ... */}>{/* ... */}</form>
}検証モード#
TanStack Form は validators オプションで複数の検証ストラテジーをサポートしています。
| モード | 説明 |
|---|---|
"onChange" | 変更ごとに検証が実行されます。 |
"onBlur" | blur 時に検証が実行されます。 |
"onSubmit" | 送信時に検証が実行されます。 |
const form = useForm({
defaultValues: {
title: "",
description: "",
},
validators: {
onSubmit: formSchema,
onChange: formSchema,
onBlur: formSchema,
},
})エラーの表示#
<FieldError /> でフィールドの横にエラーを表示します。スタイリングとアクセシビリティのため:
<Field />コンポーネントにdata-invalidを付けます。<Input />、<SelectTrigger />、<Checkbox />などのフォームコントロールにaria-invalidを付けます。
<form.Field
name="email"
children={(field) => {
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid
return (
<Field data-invalid={isInvalid}>
<FieldLabel htmlFor={field.name}>Email</FieldLabel>
<Input
id={field.name}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
type="email"
aria-invalid={isInvalid}
/>
{isInvalid && <FieldError errors={field.state.meta.errors} />}
</Field>
)
}}
/>フィールドタイプ別の使い方#
Input#
- 入力フィールドには
<Input />にfield.state.valueとfield.handleChangeを使用します。 - エラー表示には
<Input />にaria-invalid、<Field />にdata-invalidを付けます。
Component form-tanstack-input not found in registry.
<form.Field
name="username"
children={(field) => {
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid
return (
<Field data-invalid={isInvalid}>
<FieldLabel htmlFor="form-tanstack-input-username">Username</FieldLabel>
<Input
id="form-tanstack-input-username"
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
aria-invalid={isInvalid}
placeholder="tyapme"
autoComplete="username"
/>
<FieldDescription>
This is your public display name. Must be between 3 and 10 characters.
Must only contain letters, numbers, and underscores.
</FieldDescription>
{isInvalid && <FieldError errors={field.state.meta.errors} />}
</Field>
)
}}
/>Textarea#
- テキストエリアフィールドには
<Textarea />にfield.state.valueとfield.handleChangeを使用します。 - エラー表示には
<Textarea />にaria-invalid、<Field />にdata-invalidを付けます。
Component form-tanstack-textarea not found in registry.
<form.Field
name="about"
children={(field) => {
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid
return (
<Field data-invalid={isInvalid}>
<FieldLabel htmlFor="form-tanstack-textarea-about">
More about you
</FieldLabel>
<Textarea
id="form-tanstack-textarea-about"
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
aria-invalid={isInvalid}
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>
{isInvalid && <FieldError errors={field.state.meta.errors} />}
</Field>
)
}}
/>Select#
- Select コンポーネントには
<Select />にfield.state.valueとfield.handleChangeを使用します。 - エラー表示には
<SelectTrigger />にaria-invalid、<Field />にdata-invalidを付けます。
Component form-tanstack-select not found in registry.
<form.Field
name="language"
children={(field) => {
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid
return (
<Field orientation="responsive" data-invalid={isInvalid}>
<FieldContent>
<FieldLabel htmlFor="form-tanstack-select-language">
Spoken Language
</FieldLabel>
<FieldDescription>
For best results, select the language you speak.
</FieldDescription>
{isInvalid && <FieldError errors={field.state.meta.errors} />}
</FieldContent>
<Select
name={field.name}
value={field.state.value}
onValueChange={field.handleChange}
>
<SelectTrigger
id="form-tanstack-select-language"
aria-invalid={isInvalid}
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>
)
}}
/>Checkbox#
- チェックボックスには
<Checkbox />にfield.state.valueとfield.handleChangeを使用します。 - エラー表示には
<Checkbox />にaria-invalid、<Field />にdata-invalidを付けます。 - チェックボックス配列には
<form.Field />にmode="array"を使用し、TanStack Form の配列ヘルパーを利用します。 - 適切なスタイリングと間距のため、
<FieldGroup />にdata-slot="checkbox-group"を付けてください。
Component form-tanstack-checkbox not found in registry.
<form.Field
name="tasks"
mode="array"
children={(field) => {
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid
return (
<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={isInvalid}
>
<Checkbox
id={`form-tanstack-checkbox-${task.id}`}
name={field.name}
aria-invalid={isInvalid}
checked={field.state.value.includes(task.id)}
onCheckedChange={(checked) => {
if (checked) {
field.pushValue(task.id)
} else {
const index = field.state.value.indexOf(task.id)
if (index > -1) {
field.removeValue(index)
}
}
}}
/>
<FieldLabel
htmlFor={`form-tanstack-checkbox-${task.id}`}
className="font-normal"
>
{task.label}
</FieldLabel>
</Field>
))}
</FieldGroup>
{isInvalid && <FieldError errors={field.state.meta.errors} />}
</FieldSet>
)
}}
/>Radio Group#
- ラジオグループには
<RadioGroup />にfield.state.valueとfield.handleChangeを使用します。 - エラー表示には
<RadioGroupItem />にaria-invalid、<Field />にdata-invalidを付けます。
Component form-tanstack-radiogroup not found in registry.
<form.Field
name="plan"
children={(field) => {
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid
return (
<FieldSet>
<FieldLegend>Plan</FieldLegend>
<FieldDescription>
You can upgrade or downgrade your plan at any time.
</FieldDescription>
<RadioGroup
name={field.name}
value={field.state.value}
onValueChange={field.handleChange}
>
{plans.map((plan) => (
<FieldLabel
key={plan.id}
htmlFor={`form-tanstack-radiogroup-${plan.id}`}
>
<Field orientation="horizontal" data-invalid={isInvalid}>
<FieldContent>
<FieldTitle>{plan.title}</FieldTitle>
<FieldDescription>{plan.description}</FieldDescription>
</FieldContent>
<RadioGroupItem
value={plan.id}
id={`form-tanstack-radiogroup-${plan.id}`}
aria-invalid={isInvalid}
/>
</Field>
</FieldLabel>
))}
</RadioGroup>
{isInvalid && <FieldError errors={field.state.meta.errors} />}
</FieldSet>
)
}}
/>Switch#
- スイッチには
<Switch />にfield.state.valueとfield.handleChangeを使用します。 - エラー表示には
<Switch />にaria-invalid、<Field />にdata-invalidを付けます。
Component form-tanstack-switch not found in registry.
<form.Field
name="twoFactor"
children={(field) => {
const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid
return (
<Field orientation="horizontal" data-invalid={isInvalid}>
<FieldContent>
<FieldLabel htmlFor="form-tanstack-switch-twoFactor">
Multi-factor authentication
</FieldLabel>
<FieldDescription>
Enable multi-factor authentication to secure your account.
</FieldDescription>
{isInvalid && <FieldError errors={field.state.meta.errors} />}
</FieldContent>
<Switch
id="form-tanstack-switch-twoFactor"
name={field.name}
checked={field.state.value}
onCheckedChange={field.handleChange}
aria-invalid={isInvalid}
/>
</Field>
)
}}
/>Complex Forms#
複数のフィールドと検証を持つ、より複雑なフォームの例。
Component form-tanstack-complex not found in registry.
フォームのリセット#
form.reset() でフォームをデフォルト値にリセットします。
<Button type="button" variant="outline" onClick={() => form.reset()}>
Reset
</Button>配列フィールド#
TanStack Form は mode="array" による強力な配列フィールド管理を提供します。完全な検証サポート付きで、配列アイテムの動的な追加・削除・更新が可能です。
Component form-tanstack-array not found in registry.
この例では、配列フィールドを使った複数メールアドレスの管理を示します。最大5件のメールアドレスを追加でき、個別のアドレスを削除でき、各アドレスは個別に検証されます。
配列フィールドの構造#
親フィールドに mode="array" を付けて配列フィールド管理を有効にします。
<form.Field
name="emails"
mode="array"
children={(field) => {
return (
<FieldSet>
<FieldLegend variant="label">Email Addresses</FieldLegend>
<FieldDescription>
Add up to 5 email addresses where we can contact you.
</FieldDescription>
<FieldGroup>
{field.state.value.map((_, index) => (
// Nested field for each array item
))}
</FieldGroup>
</FieldSet>
)
}}
/>ネストされたフィールド#
fieldName[index].propertyName のブラケット表記で個別配列アイテムにアクセスします。この例では InputGroup で削除ボタンを入力エリア内にインライン表示します。
<form.Field
name={`emails[${index}].address`}
children={(subField) => {
const isSubFieldInvalid =
subField.state.meta.isTouched && !subField.state.meta.isValid
return (
<Field orientation="horizontal" data-invalid={isSubFieldInvalid}>
<FieldContent>
<InputGroup>
<InputGroupInput
id={`form-tanstack-array-email-${index}`}
name={subField.name}
value={subField.state.value}
onBlur={subField.handleBlur}
onChange={(e) => subField.handleChange(e.target.value)}
aria-invalid={isSubFieldInvalid}
placeholder="name@example.com"
type="email"
/>
{field.state.value.length > 1 && (
<InputGroupAddon align="inline-end">
<InputGroupButton
type="button"
variant="ghost"
size="icon-xs"
onClick={() => field.removeValue(index)}
aria-label={`Remove email ${index + 1}`}
>
<XIcon />
</InputGroupButton>
</InputGroupAddon>
)}
</InputGroup>
{isSubFieldInvalid && (
<FieldError errors={subField.state.meta.errors} />
)}
</FieldContent>
</Field>
)
}}
/>アイテムの追加#
field.pushValue(item) で配列フィールドにアイテムを追加します。配列が最大長に達したときにボタンを無効化できます。
<Button
type="button"
variant="outline"
size="sm"
onClick={() => field.pushValue({ address: "" })}
disabled={field.state.value.length >= 5}
>
Add Email Address
</Button>アイテムの削除#
field.removeValue(index) で配列フィールドからアイテムを削除します。アイテムが2件以上のときのみ削除ボタンを表示するようにできます。
{
field.state.value.length > 1 && (
<InputGroupButton
onClick={() => field.removeValue(index)}
aria-label={`Remove email ${index + 1}`}
>
<XIcon />
</InputGroupButton>
)
}配列の検証#
Zod の配列メソッドで配列フィールドを検証します。
const formSchema = z.object({
emails: z
.array(
z.object({
address: z.string().email("Enter a valid email address."),
})
)
.min(1, "Add at least one email address.")
.max(5, "You can add up to 5 email addresses."),
})