概要
ファイル選択後、サーバーに送信する前にクライアント側で画像プレビューを表示するコンポーネント。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に渡せばドラッグ後にプレビュー表示が実現できる