レビュー済み·難易度: 中級·更新: 2026-04-16

MUI + React Hook Form で入力フォームを組む

MUI の TextField / Button を react-hook-form の Controller でラップし、Zod でバリデーションする実装例。MUI の非標準 input と react-hook-form を正しく接続する方法を扱う。

reactformvalidationui-componentmuireact-hook-formzod

対応バージョン

react 19typescript 5@mui/material 6react-hook-form 7zod 3

前提環境

React の基本的な使い方と react-hook-form の基礎(register / handleSubmit)を理解していること

概要

MUI(Material UI)のフォームコンポーネントは <input> を直接 DOM に露出しないため、react-hook-formregister() が正しく機能しない。 Controller コンポーネントを使って MUI の onChange / onBlur / value を接続するのが正しいパターン。

インストール

npm install @mui/material @emotion/react @emotion/styled
npm install react-hook-form @hookform/resolvers zod

スキーマ定義

// src/lib/schemas/profile.ts
import { z } from "zod";

export const profileSchema = z.object({
  name: z.string().min(1, "名前を入力してください"),
  email: z.string().email("有効なメールアドレスを入力してください"),
  bio: z.string().max(200, "200 文字以内で入力してください").optional(),
});

export type ProfileInput = z.infer<typeof profileSchema>;

Controller を使ったフォーム実装

// src/components/ProfileForm.tsx
"use client";

import { useForm, Controller } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import TextField from "@mui/material/TextField";
import Button from "@mui/material/Button";
import Stack from "@mui/material/Stack";
import { profileSchema, type ProfileInput } from "@/lib/schemas/profile";

export function ProfileForm() {
  const {
    control,
    handleSubmit,
    formState: { errors, isSubmitting },
  } = useForm<ProfileInput>({
    resolver: zodResolver(profileSchema),
    defaultValues: { name: "", email: "", bio: "" },
  });

  const onSubmit = async (data: ProfileInput) => {
    // TODO: API 呼び出し
    console.log(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <Stack spacing={2} sx={{ maxWidth: 400 }}>
        <Controller
          name="name"
          control={control}
          render={({ field }) => (
            <TextField
              {...field}
              label="名前"
              error={!!errors.name}
              helperText={errors.name?.message}
              fullWidth
            />
          )}
        />

        <Controller
          name="email"
          control={control}
          render={({ field }) => (
            <TextField
              {...field}
              label="メールアドレス"
              type="email"
              error={!!errors.email}
              helperText={errors.email?.message}
              fullWidth
            />
          )}
        />

        <Controller
          name="bio"
          control={control}
          render={({ field }) => (
            <TextField
              {...field}
              label="自己紹介(任意)"
              multiline
              rows={3}
              error={!!errors.bio}
              helperText={errors.bio?.message ?? "200 文字以内"}
              fullWidth
            />
          )}
        />

        <Button
          type="submit"
          variant="contained"
          disabled={isSubmitting}
          fullWidth
        >
          {isSubmitting ? "送信中..." : "保存する"}
        </Button>
      </Stack>
    </form>
  );
}

register() が使えない理由

// NG: MUI の TextField は ref を input に直接渡さないため動作しない
<TextField {...register("name")} />

// OK: Controller を介して MUI の field props と接続する
<Controller
  name="name"
  control={control}
  render={({ field }) => <TextField {...field} />}
/>

Controllerrender には { field, fieldState } が渡される。 field には onChange / onBlur / value / ref / name が含まれており、MUI の props と正しく対応する。

MuiTextField に defaultValues を渡す理由

// defaultValues を省略すると value が undefined になり、
// MUI が「制御なし → 制御あり」の切り替え警告を出す
useForm<ProfileInput>({
  defaultValues: { name: "", email: "", bio: "" },
});

defaultValues を設定することで MUI コンポーネントを常に controlled として扱える。

ポイント

  • MUI / Radix UI / shadcn/ui のような UI ライブラリは Controller パターンを使う
  • helperTexterrors.xxx?.message を渡すとバリデーションエラーを自然に表示できる
  • error={!!errors.xxx} で MUI の赤色スタイルが適用される
  • defaultValues を必ず設定し、uncontrolled → controlled の切り替えを防ぐ

注意点

MUI の TextField は内部で独自の input 要素を持つため、register() ではなく Controller を使う必要がある

関連サンプル