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

React Hook Form でファイルアップロード対応フォームを実装する

React Hook Form の register でファイル入力を管理し、Zod でファイルサイズ・拡張子バリデーションを行う実装例。

nextjsformfile-uploadvalidationreact-hook-formzod

対応バージョン

nextjs 15react 19react-hook-form 7zod 3

前提環境

React Hook Form と Zod の基本的な使い方を理解していること

概要

React Hook Form の register でファイル入力(<input type="file">)を管理し、 Zod でファイルサイズや拡張子をバリデーションする。 z.instanceof(FileList) を使うことで FileList を型安全に扱える。

インストール

npm install react-hook-form zod @hookform/resolvers

実装例

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

import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { useEffect, useState } from "react";

const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
const ACCEPTED_IMAGE_TYPES = ["image/jpeg", "image/png", "image/webp"];

const schema = z.object({
  file: z
    .instanceof(FileList)
    .refine((files) => files.length > 0, "ファイルを選択してください")
    .refine(
      (files) => files[0].size <= MAX_FILE_SIZE,
      "ファイルサイズは 5MB 以下にしてください"
    )
    .refine(
      (files) => ACCEPTED_IMAGE_TYPES.includes(files[0].type),
      "JPEG / PNG / WebP のみアップロードできます"
    ),
});

type FormValues = z.infer<typeof schema>;

export function FileUploadForm() {
  const [preview, setPreview] = useState<string | null>(null);

  const {
    register,
    handleSubmit,
    watch,
    formState: { errors, isSubmitting },
  } = useForm<FormValues>({
    resolver: zodResolver(schema),
  });

  const fileList = watch("file");

  useEffect(() => {
    if (!fileList || fileList.length === 0) return;

    const url = URL.createObjectURL(fileList[0]);
    setPreview(url);

    // コンポーネント unmount 時にオブジェクト URL を解放する
    return () => URL.revokeObjectURL(url);
  }, [fileList]);

  const onSubmit = (data: FormValues) => {
    const formData = new FormData();
    formData.append("file", data.file[0]);

    // TODO: formData をサーバーへ送信する
    console.log("送信:", data.file[0].name);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
      <div>
        <label htmlFor="file" className="block text-sm font-medium">
          画像ファイル(JPEG / PNG / WebP・5MB 以下)
        </label>
        <input
          id="file"
          type="file"
          accept="image/jpeg,image/png,image/webp"
          className="mt-1 block text-sm"
          {...register("file")}
        />
        {errors.file && (
          <p className="mt-1 text-xs text-red-600">{errors.file.message}</p>
        )}
      </div>

      {preview && (
        <img
          src={preview}
          alt="プレビュー"
          className="h-32 w-32 rounded object-cover"
        />
      )}

      <button
        type="submit"
        disabled={isSubmitting}
        className="rounded bg-blue-600 px-4 py-2 text-sm text-white disabled:opacity-50"
      >
        アップロード
      </button>
    </form>
  );
}

ポイント

  • z.instanceof(FileList)FileList 型を受け取り、.refine() でサイズ・拡張子を検証する
  • watch("file") で入力の変化を監視し、URL.createObjectURL でプレビューを生成する
  • useEffect の return で URL.revokeObjectURL を呼び、メモリリークを防ぐ
  • サーバーへの送信は FormData に変換して渡す(Server Actions / API Route いずれにも対応)
  • @hookform/resolverszodResolver で Zod スキーマを RHF に接続する

注意点

FileList は Zod で直接検証できないため z.instanceof(FileList) を使う。Server Actions と組み合わせる場合は FormData に変換して渡す。プレビュー表示には URL.createObjectURL を使い、コンポーネント unmount 時に URL.revokeObjectURL で解放する。

関連サンプル