フォームの状態管理
フォームの状態管理は、入力値・バリデーション結果・送信状態などを一元管理する仕組み。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)の管理
- •フィールドレベルバリデーション
ライブラリ横断比較
| 機能 | Mantine | Ant Design | shadcn/ui | MUI |
|---|---|---|---|---|
| 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")