レビュー待ち·難易度: 中級·更新: 2026-04-18

Next.js でファイルアップロードの進捗バーを表示する

XMLHttpRequest の progress イベントを使ってアップロード中の進捗率を取得し、プログレスバー UI でリアルタイム表示する実装例。

nextjsfile-uploadui-component

対応バージョン

nextjs 15react 19

前提環境

React の useState と XMLHttpRequest の基本を理解していること

概要

fetch API はアップロード進捗を取得できないため、XMLHttpRequestupload.onprogress イベントを使って進捗率を取得しプログレスバーで表示する。キャンセル機能も合わせて実装する。

インストール

# 追加インストールは不要

実装

アップロード API(Route Handler)

// app/api/upload/route.ts
import { NextResponse } from "next/server";

export async function POST(request: Request) {
  const formData = await request.formData();
  const file = formData.get("file");

  if (!file || typeof file === "string") {
    return NextResponse.json({ error: "ファイルが必要です" }, { status: 400 });
  }

  // 実際のアプリではここでストレージへ保存する
  // await saveToStorage(file)

  return NextResponse.json({ message: "アップロード完了" });
}

進捗付きアップロードコンポーネント

// components/UploadWithProgress.tsx
"use client";

import { useRef, useState } from "react";

type UploadState = "idle" | "uploading" | "done" | "error" | "cancelled";

export function UploadWithProgress() {
  const [progress, setProgress] = useState(0);
  const [status, setStatus] = useState<UploadState>("idle");
  const [message, setMessage] = useState<string | null>(null);
  const xhrRef = useRef<XMLHttpRequest | null>(null);

  function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
    const file = e.target.files?.[0];
    if (!file) return;
    uploadFile(file);
  }

  function uploadFile(file: File) {
    const xhr = new XMLHttpRequest();
    xhrRef.current = xhr;

    const formData = new FormData();
    formData.append("file", file);

    // 進捗イベント: loaded / total から % を計算
    xhr.upload.onprogress = (e) => {
      if (e.lengthComputable) {
        setProgress(Math.round((e.loaded / e.total) * 100));
      }
    };

    xhr.onloadstart = () => {
      setStatus("uploading");
      setProgress(0);
      setMessage(null);
    };

    xhr.onload = () => {
      if (xhr.status >= 200 && xhr.status < 300) {
        setStatus("done");
        setMessage("アップロードが完了しました");
      } else {
        setStatus("error");
        setMessage("アップロードに失敗しました");
      }
    };

    xhr.onerror = () => {
      setStatus("error");
      setMessage("ネットワークエラーが発生しました");
    };

    xhr.onabort = () => {
      setStatus("cancelled");
      setProgress(0);
      setMessage("キャンセルしました");
    };

    xhr.open("POST", "/api/upload");
    xhr.send(formData);
  }

  function handleCancel() {
    xhrRef.current?.abort();
  }

  return (
    <div className="space-y-4">
      <input
        type="file"
        onChange={handleChange}
        disabled={status === "uploading"}
        className="block text-sm text-gray-600 file:mr-4 file:rounded file:border-0 file:bg-blue-50 file:px-4 file:py-2 file:text-sm file:font-medium file:text-blue-700 hover:file:bg-blue-100 disabled:opacity-50"
      />

      {status === "uploading" && (
        <div className="space-y-2">
          <div className="h-2 w-full overflow-hidden rounded-full bg-gray-200">
            <div
              className="h-full rounded-full bg-blue-500 transition-all duration-200"
              style={{ width: `${progress}%` }}
            />
          </div>
          <div className="flex items-center justify-between text-xs text-gray-500">
            <span>{progress}%</span>
            <button
              type="button"
              onClick={handleCancel}
              className="rounded bg-gray-100 px-2 py-1 hover:bg-gray-200"
            >
              キャンセル
            </button>
          </div>
        </div>
      )}

      {message && (
        <p
          className={`text-sm ${
            status === "done" ? "text-green-600" : "text-red-500"
          }`}
        >
          {message}
        </p>
      )}
    </div>
  );
}

ページへの組み込み

// app/page.tsx
import { UploadWithProgress } from "@/components/UploadWithProgress";

export default function Page() {
  return (
    <main className="mx-auto max-w-md p-8">
      <h1 className="mb-6 text-2xl font-bold">ファイルアップロード</h1>
      <UploadWithProgress />
    </main>
  );
}

ポイント

  • fetchupload.onprogress に対応していないため進捗を取得できない。進捗表示が必要な場合は XMLHttpRequest を使う
  • xhr.upload.onprogress イベントの e.loaded / e.total から進捗率を計算する。e.lengthComputablefalse の場合は合計サイズが不明なため計算をスキップする
  • xhr.abort() でアップロードを中断できる。onabort イベントが発火するので UI を「キャンセル済み」に更新する
  • useRef で XHR インスタンスを保持する。useState にすると状態更新のたびに再レンダリングが走るため ref が適切
  • プログレスバーは width: progress + "%" をインラインスタイルで設定する。Tailwind の動的クラス(w-[N%])はビルド時にパージされる可能性があるためインラインスタイルが安全

注意点

nextjs-file-upload-api はサーバー側の受け取り処理(FormData POST)。react-file-drop-upload はドラッグ&ドロップ UI。react-image-preview-upload はプレビュー表示。これはアップロード中の進捗率取得と progress bar UI に特化。fetch では進捗が取れないため XHR を使う点が主眼。

関連サンプル