フォームの状態管理

フォームの状態管理は、入力値・バリデーション結果・送信状態などを一元管理する仕組み。React Hook FormやZodなどの外部ライブラリと組み合わせて使うケースが多く、UIライブラリごとに連携方法が異なる。

Mantine・Ant Design は独自のフォーム管理フック(useForm / Form.useForm)を内蔵しており、ライブラリ単体で完結できる。shadcn/ui・MUI は React Hook Form との組み合わせを推奨しており、Zod による型安全なスキーマバリデーションと相性が良い。

主なバリエーション
  • controlled(制御コンポーネント)/ uncontrolled(非制御コンポーネント)
  • React Hook Form との連携
  • Zod スキーマによる型安全なバリデーション
  • フォームの初期値・リセット
  • 送信状態(loading / disabled)の管理
  • フィールドレベルバリデーション

ライブラリ横断比較

機能MantineAnt Designshadcn/uiMUI
controlled / uncontrolled
useForm
Form.useForm
両対応
両対応
React Hook Form 連携
公式サポート
手動統合
公式推奨
Controller
独自フォーム管理(useForm 等)
@mantine/form
rc-field-form
××
Zod スキーマ連携
zodResolver
手動実装
zodResolver
手動実装
フォーム初期値・リセット
initialValues/reset
initialValues/resetFields
defaultValues/reset
defaultValues/reset
送信状態の管理
form.submitting
useState
isSubmitting
isSubmitting
フィールドレベルバリデーション
validate[field]
rules
validate
validate

○ = 対応  △ = 部分対応・制限あり  × = 非対応

ライブラリ別コード例

各ライブラリでフォームの状態管理を実装する際の設定部分を抜粋しています。 動くデモは各比較ページでご確認ください。

Mantine

// @mantine/form で状態管理
import { useForm } from '@mantine/form';
import { TextInput, PasswordInput, Button, Stack, Switch } from '@mantine/core';

function LoginForm() {
  const form = useForm({
    initialValues: { email: '', password: '', rememberMe: false },
    validate: {
      email: (v) => /^\S+@\S+$/.test(v) ? null : '有効なメールアドレスを入力してください',
      password: (v) => v.length < 8 ? 'パスワードは8文字以上です' : null,
    },
    validateInputOnChange: true,  // リアルタイムバリデーション
  });

  return (
    <form onSubmit={form.onSubmit(async (values) => console.log(values))}>
      <Stack>
        <TextInput label="メール" {...form.getInputProps('email')} />
        <PasswordInput label="パスワード" {...form.getInputProps('password')} />
        <Switch
          label="ログインを保持"
          {...form.getInputProps('rememberMe', { type: 'checkbox' })}
        />
        <Button type="submit" disabled={!form.isValid()} loading={form.submitting}>
          ログイン
        </Button>
        <Button variant="subtle" onClick={() => form.reset()}>リセット</Button>
      </Stack>
    </form>
  );
}

// Zod 連携(mantine-form-zod-resolver)
import { zodResolver } from 'mantine-form-zod-resolver';
import { z } from 'zod';

const schema = z.object({
  email: z.string().email('有効なメールアドレスを入力してください'),
  age: z.number().min(18, '18歳以上のみ登録可能です'),
});
const zodForm = useForm({
  validate: zodResolver(schema),
  initialValues: { email: '', age: 0 },
});

Ant Design

// Ant Design の Form で状態管理(rc-field-form ベース)
import { Form, Input, Checkbox, Button } from 'antd';
import { useState } from 'react';

type FieldType = { email: string; password: string; remember: boolean };

function LoginForm() {
  const [form] = Form.useForm<FieldType>();
  const [loading, setLoading] = useState(false);

  const onFinish = async (values: FieldType) => {
    setLoading(true);
    try { await submitAPI(values); }
    finally { setLoading(false); }
  };

  return (
    <Form
      form={form}
      onFinish={onFinish}
      layout="vertical"
      initialValues={{ remember: false }}
    >
      <Form.Item name="email" label="メール"
        rules={[
          { required: true },
          { type: 'email', message: '有効なメールアドレスを入力してください' },
        ]}
        validateTrigger="onChange"
      >
        <Input />
      </Form.Item>
      <Form.Item name="password" label="パスワード"
        rules={[{ required: true }, { min: 8, message: 'パスワードは8文字以上です' }]}
      >
        <Input.Password />
      </Form.Item>
      <Form.Item name="remember" valuePropName="checked">
        <Checkbox>ログインを保持</Checkbox>
      </Form.Item>
      <Form.Item>
        <Button type="primary" htmlType="submit" loading={loading}>ログイン</Button>
        <Button onClick={() => form.resetFields()} style={{ marginLeft: 8 }}>
          リセット
        </Button>
      </Form.Item>
    </Form>
  );
}

shadcn/ui

// shadcn/ui は React Hook Form + Zod を公式推奨
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import {
  Form, FormControl, FormField, FormItem, FormLabel, FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';

const schema = z.object({
  email: z.string().email('有効なメールアドレスを入力してください'),
  password: z.string().min(8, 'パスワードは8文字以上です'),
});
type Schema = z.infer<typeof schema>;

function LoginForm() {
  const form = useForm<Schema>({
    resolver: zodResolver(schema),
    defaultValues: { email: '', password: '' },
  });

  const onSubmit = async (values: Schema) => {
    console.log(values);
  };

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
        <FormField control={form.control} name="email" render={({ field }) => (
          <FormItem>
            <FormLabel>メール</FormLabel>
            <FormControl><Input {...field} /></FormControl>
            <FormMessage />  {/* Zodエラーを自動表示 */}
          </FormItem>
        )} />
        <FormField control={form.control} name="password" render={({ field }) => (
          <FormItem>
            <FormLabel>パスワード</FormLabel>
            <FormControl><Input type="password" {...field} /></FormControl>
            <FormMessage />
          </FormItem>
        )} />
        <div className="flex gap-2">
          <Button type="submit" disabled={form.formState.isSubmitting}>
            {form.formState.isSubmitting ? '送信中...' : 'ログイン'}
          </Button>
          <Button type="button" variant="outline" onClick={() => form.reset()}>
            リセット
          </Button>
        </div>
      </form>
    </Form>
  );
}

MUI

// MUI は React Hook Form + Controller で統合
import { useForm, Controller } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { TextField, Button, Checkbox, FormControlLabel, Stack } from '@mui/material';
import { LoadingButton } from '@mui/lab';

const schema = z.object({
  email: z.string().email('有効なメールアドレスを入力してください'),
  password: z.string().min(8, 'パスワードは8文字以上です'),
  remember: z.boolean(),
});
type Schema = z.infer<typeof schema>;

function LoginForm() {
  const {
    control, handleSubmit, reset,
    formState: { isSubmitting },
  } = useForm<Schema>({
    resolver: zodResolver(schema),
    defaultValues: { email: '', password: '', remember: false },
  });

  return (
    <form onSubmit={handleSubmit(console.log)}>
      <Stack spacing={2}>
        <Controller name="email" control={control} render={({ field, fieldState }) => (
          <TextField
            {...field}
            label="メール"
            error={!!fieldState.error}
            helperText={fieldState.error?.message}
          />
        )} />
        <Controller name="password" control={control} render={({ field, fieldState }) => (
          <TextField
            {...field}
            type="password"
            label="パスワード"
            error={!!fieldState.error}
            helperText={fieldState.error?.message}
          />
        )} />
        <Controller name="remember" control={control} render={({ field }) => (
          <FormControlLabel
            control={<Checkbox {...field} checked={field.value} />}
            label="ログインを保持"
          />
        )} />
        <Stack direction="row" spacing={1}>
          <LoadingButton type="submit" loading={isSubmitting} variant="contained">
            ログイン
          </LoadingButton>
          <Button variant="outlined" onClick={() => reset()}>リセット</Button>
        </Stack>
      </Stack>
    </form>
  );
}

まとめ・選び方のヒント

  • 外部ライブラリなしでフォームを管理したい → Mantine(@mantine/form が強力で Zod 連携も容易)・Ant Design(Form.useForm で完結)
  • React Hook Form + Zod で型安全に構築したい → shadcn/ui(公式ドキュメントでの推奨構成)・MUI(Controller で任意コンポーネントを統合)
  • 送信中のローディング状態を楽に管理したい → Mantine(form.submitting で自動管理)・shadcn/ui・MUI(React Hook Form の isSubmitting
  • バリデーションをリアルタイムで走らせたい → Mantine(validateInputOnChange)・Ant Design(validateTrigger="onChange"