125k

TanStack Form

TanStack Form と Zod を使用して React でフォームを構築します。

このガイドでは、TanStack Form を使用してフォームを構築する方法を説明します。 <Field /> コンポーネントを使用してフォームを作成し、Zod を使用してスキーマ検証を実装し、エラーを処理し、アクセシビリティを確保する方法を学びます。

デモ

まずは次のフォームを構築します。単純なテキスト入力とテキストエリアがあります。送信時にフォーム データを検証し、エラーがあれば表示します。

コンポーネント form-tanstack-demo が見つかりません。

アプローチ

このフォームは 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 スキーマを使用してフォームの形状を定義します。

form.tsx
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 検証付きのフォーム インスタンスを作成します。

form.tsx
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 は他の検証モードをサポートしています。詳細については、ドキュメント を参照してください。

フォームの構築

TanStack Form の form.Field コンポーネントと <Field /> コンポーネントを使用してフォームを構築できるようになりました。

完了

以上です。これで、クライアント側の検証を備えた完全にアクセス可能なフォームが完成しました。

フォームを送信すると、検証されたフォーム データを使用して onSubmit 関数が呼び出されます。フォーム データが無効な場合、TanStack Form は各フィールドの横にエラーを表示します。

バリデーション

クライアント側検証

TanStack Form は Zod スキーマを使ってフォーム データを検証します。ユーザーの入力中にリアルタイムで検証されます。

form.tsx
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"フォーカスが外れたときに検証が実行されます。
"onSubmit"送信時に検証がトリガーされます。
form.tsx
const form = useForm({
  defaultValues: {
    title: "",
    description: "",
  },
  validators: {
    onSubmit: formSchema,
    onChange: formSchema,
    onBlur: formSchema,
  },
})

エラーの表示

<FieldError /> を使用してフィールドの横にエラーを表示します。スタイルとアクセシビリティについて:

  • data-invalid プロパティを <Field /> コンポーネントに追加します。
  • <Input />、<SelectTrigger />、<Checkbox /> などのフォーム コントロールに aria-invalid プロパティを追加します。
form.tsx
<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 /> コンポーネントの field.state.value および field.handleChange を使用します。
  • エラーを表示するには、aria-invalid プロパティを <Input /> コンポーネントに追加し、data-invalid プロパティを <Field /> コンポーネントに追加します。

コンポーネント form-tanstack-input が見つかりません。

form.tsx
<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="shadcn"
          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 /> コンポーネントの field.state.value および field.handleChange を使用します。
  • エラーを表示するには、aria-invalid プロパティを <Textarea /> コンポーネントに追加し、data-invalid プロパティを <Field /> コンポーネントに追加します。

コンポーネント form-tanstack-textarea が見つかりません。

form.tsx
<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 /> コンポーネントで field.state.value および field.handleChange を使用します。
  • エラーを表示するには、aria-invalid プロパティを <SelectTrigger /> コンポーネントに追加し、data-invalid プロパティを <Field /> コンポーネントに追加します。

コンポーネント form-tanstack-select が見つかりません。

form.tsx
<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 /> コンポーネントの field.state.value および field.handleChange を使用します。
  • エラーを表示するには、aria-invalid プロパティを <Checkbox /> コンポーネントに追加し、data-invalid プロパティを <Field /> コンポーネントに追加します。
  • チェックボックス配列の場合は、<form.Field /> コンポーネントと TanStack Form の配列ヘルパーで mode="array" を使用します。
  • 適切なスタイルと間隔を確保するには、必ず data-slot="checkbox-group" を <FieldGroup /> コンポーネントに追加してください。

コンポーネント form-tanstack-checkbox が見つかりません。

form.tsx
<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&apos;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>
    )
  }}
/>

ラジオ グループ

  • ラジオ グループの場合は、<RadioGroup /> コンポーネントで field.state.value および field.handleChange を使用します。
  • エラーを表示するには、aria-invalid プロパティを <RadioGroupItem /> コンポーネントに追加し、data-invalid プロパティを <Field /> コンポーネントに追加します。

コンポーネント form-tanstack-radiogroup が見つかりません。

form.tsx
<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 /> コンポーネントで field.state.value と field.handleChange を使います。
  • エラーを表示するには、aria-invalid プロパティを <Switch /> コンポーネントに追加し、data-invalid プロパティを <Field /> コンポーネントに追加します。

コンポーネント form-tanstack-switch が見つかりません。

form.tsx
<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>
    )
  }}
/>

複雑なフォーム

複数のフィールドと検証を含む、より複雑なフォームの例です。

コンポーネント form-tanstack-complex が見つかりません。

フォームをリセット

form.reset() を使用してフォームをデフォルト値にリセットします。

<Button type="button" variant="outline" onClick={() => form.reset()}>
  Reset
</Button>

配列フィールド

TanStack Form は、mode="array" を使用した強力な配列フィールド管理を提供します。これにより、完全な検証サポートを使用して配列項目を動的に追加、削除、更新できるようになります。

コンポーネント form-tanstack-array が見つかりません。

この例では、配列フィールドを使用して複数の電子メール アドレスを管理する方法を示します。ユーザーは最大 5 つの電子メール アドレスを追加したり、個々のアドレスを削除したりできます。各アドレスは個別に検証されます。

配列フィールド構造

配列フィールド管理を有効にするには、親フィールドで mode="array" を使用します。

form.tsx
<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.tsx
<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) を使って配列フィールドに項目を追加します。配列が最大長に達したらボタンを無効にできます。

form.tsx
<Button
  type="button"
  variant="outline"
  size="sm"
  onClick={() => field.pushValue({ address: "" })}
  disabled={field.state.value.length >= 5}
>
  Add Email Address
</Button>

項目の削除

field.removeValue(index) を使用して、配列フィールドから項目を削除します。複数の項目がある場合にのみ、条件付きで削除ボタンを表示できます。

form.tsx
{
  field.state.value.length > 1 && (
    <InputGroupButton
      onClick={() => field.removeValue(index)}
      aria-label={`Remove email ${index + 1}`}
    >
      <XIcon />
    </InputGroupButton>
  )
}

配列の検証

Zod の配列メソッドを使って配列フィールドを検証します。

form.tsx
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."),
})