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

React でファイル選択時に画像プレビューを表示する

FileReader や URL.createObjectURL を使い、ファイル選択直後にクライアント側で画像プレビューを表示するコンポーネント実装例。

nextjsfile-uploadui-component

対応バージョン

nextjs 15react 19

前提環境

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

概要

ファイル選択後、サーバーに送信する前にクライアント側で画像プレビューを表示するコンポーネント。URL.createObjectURL を使ったシンプルな方法と、FileReader を使って Base64 文字列に変換する方法の両方を示す。

インストール

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

実装

URL.createObjectURL を使ったプレビュー(推奨)

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

import { useEffect, useRef, useState } from "react";

export function ImagePreview() {
  const [previewUrl, setPreviewUrl] = useState<string | null>(null);
  const [file, setFile] = useState<File | null>(null);
  const inputRef = useRef<HTMLInputElement>(null);

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

    // 旧 URL を解放してからセット(メモリリーク防止)
    if (previewUrl) URL.revokeObjectURL(previewUrl);

    setFile(selected);
    setPreviewUrl(URL.createObjectURL(selected));
  }

  // コンポーネントのアンマウント時に URL を解放
  useEffect(() => {
    return () => {
      if (previewUrl) URL.revokeObjectURL(previewUrl);
    };
  }, [previewUrl]);

  function handleClear() {
    if (previewUrl) URL.revokeObjectURL(previewUrl);
    setPreviewUrl(null);
    setFile(null);
    if (inputRef.current) inputRef.current.value = "";
  }

  return (
    <div className="space-y-4">
      <input
        ref={inputRef}
        type="file"
        accept="image/*"
        onChange={handleChange}
        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"
      />

      {previewUrl && (
        <div className="space-y-2">
          <img
            src={previewUrl}
            alt="プレビュー"
            className="max-h-64 rounded border object-contain"
          />
          <p className="text-xs text-gray-500">
            {file?.name} ({Math.round((file?.size ?? 0) / 1024)} KB)
          </p>
          <button
            type="button"
            onClick={handleClear}
            className="rounded bg-gray-100 px-3 py-1 text-xs text-gray-600 hover:bg-gray-200"
          >
            クリア
          </button>
        </div>
      )}
    </div>
  );
}

FileReader を使った Base64 プレビュー

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

import { useState } from "react";

export function ImagePreviewBase64() {
  const [base64, setBase64] = useState<string | null>(null);
  const [error, setError] = useState<string | null>(null);

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

    // ファイルサイズ上限チェック(例: 5MB)
    if (file.size > 5 * 1024 * 1024) {
      setError("5MB 以下のファイルを選択してください");
      return;
    }

    setError(null);
    const reader = new FileReader();

    reader.onload = (ev) => {
      setBase64(ev.target?.result as string);
    };

    reader.onerror = () => {
      setError("ファイルの読み込みに失敗しました");
    };

    reader.readAsDataURL(file);
  }

  return (
    <div className="space-y-4">
      <input
        type="file"
        accept="image/*"
        onChange={handleChange}
        className="block text-sm text-gray-600"
      />

      {error && <p className="text-sm text-red-500">{error}</p>}

      {base64 && (
        <img
          src={base64}
          alt="プレビュー"
          className="max-h-64 rounded border object-contain"
        />
      )}
    </div>
  );
}

複数ファイルのプレビュー

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

import { useEffect, useState } from "react";

type PreviewItem = { url: string; name: string; size: number };

export function MultiImagePreview() {
  const [previews, setPreviews] = useState<PreviewItem[]>([]);

  function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
    const files = Array.from(e.target.files ?? []);

    // 既存 URL を解放
    previews.forEach((p) => URL.revokeObjectURL(p.url));

    const newPreviews = files.map((file) => ({
      url: URL.createObjectURL(file),
      name: file.name,
      size: file.size,
    }));

    setPreviews(newPreviews);
  }

  useEffect(() => {
    return () => {
      previews.forEach((p) => URL.revokeObjectURL(p.url));
    };
  }, [previews]);

  return (
    <div className="space-y-4">
      <input
        type="file"
        accept="image/*"
        multiple
        onChange={handleChange}
        className="block text-sm text-gray-600"
      />

      {previews.length > 0 && (
        <div className="grid grid-cols-3 gap-3">
          {previews.map((p) => (
            <div key={p.url} className="space-y-1">
              <img
                src={p.url}
                alt={p.name}
                className="h-24 w-full rounded border object-cover"
              />
              <p className="truncate text-xs text-gray-500">{p.name}</p>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

ポイント

  • URL.createObjectURL(file) はメモリ上の Blob URL を作成する。img src に直接セットできてシンプル。使い終わったら URL.revokeObjectURL(url) でメモリを解放する
  • FileReader.readAsDataURL は Base64 文字列を返す。サーバーへの送信や <canvas> との組み合わせに向くが、ファイルサイズが大きいと文字列も大きくなる。プレビューのみなら createObjectURL の方が効率的
  • useEffect の cleanup 関数で revokeObjectURL を呼び、コンポーネントのアンマウント時・次のファイル選択時に必ず解放する
  • accept="image/*" で画像ファイルのみ選択可能にする。バリデーションとしては file.type.startsWith("image/") でコード側でも確認する
  • react-file-drop-upload との組み合わせ: ドラッグ&ドロップで受け取ったファイルを URL.createObjectURL に渡せばドラッグ後にプレビュー表示が実現できる

注意点

react-file-drop-upload はドラッグ&ドロップ受け付け UI と hidden input クリック。nextjs-file-upload-api はサーバー送信処理。これはファイル選択後のクライアント側プレビュー表示に特化(URL.createObjectURL と FileReader の使い分けも示す)。

関連サンプル