この記事ではreactで複数の画像を選択してアップロードする方法をご紹介します。
後でご紹介するサンプルコードでは
- material UI v5
 - axios
 - typescript
 
を使用しています。
ご紹介するサンプルコードはコメントに画像を付けて投稿するフォームを想定しています。
このフォームの仕様は以下のとおりです。
- コメント用のテキスト入力欄
 - 画像を最大4枚まで選択・アップロード
 - 画像を選択したら選択中のすべての画像のプレビューを表示
 - 1つのボタンで画像を選択
 - 選択した画像は削除可能
 
サンプルコードでのポイントとなる部分もご紹介します。
サンプルコード
reactで複数の画像をアップロードするサンプルコードは以下のとおりです。
import React, { useState } from "react";
import axios from "axios";
import TextField from "@mui/material/TextField";
import Button from "@mui/material/Button";
import CircularProgress from "@mui/material/CircularProgress";
import IconButton from "@mui/material/IconButton";
import CancelIcon from "@mui/icons-material/Cancel";
export type Props = {
	...
};
const CommentForm: React.FC<Props> = (props) => {
	const [isCommentSending, setIsCommentSending] = useState(false);
	const [images, setImages] = useState<File[]>([]);
	const maxImagesUpload = 4; // 画像を最大4枚まで選択・アップロード
	const [commentText, setCommentText] = useState<string>("");
	const inputId = Math.random().toString(32).substring(2);
	const handleOnSubmit = async (e: React.SyntheticEvent): Promise<void> => {
		e.preventDefault();
		setIsCommentSending(true);
		const target = e.target as typeof e.target & {
			comment: { value: string };
		};
		const data = new FormData();
		images.map((image) => {
			data.append("images[]", image);
		});
		data.append("comment", target.comment?.value || "");
		const postedComment = await axios.post(
			'/api/v1/comments',
			data
		);
    setIsCommentSending(false);
	};
	const handleOnAddImage = (e: React.ChangeEvent<HTMLInputElement>) => {
		if (!e.target.files) return;
		setImages([...images, ...e.target.files]);
	};
	const handleOnRemoveImage = (index: number) => {
    // 選択した画像は削除可能
		const newImages = [...images];
		newImages.splice(index, 1);
		setImages(newImages);
	};
	return (
		<form action="" onSubmit={(e) => handleOnSubmit(e)}>
			<TextField
				name="comment"
				value={commentText}
				multiline
				minRows={1}
				maxRows={20}
				placeholder="コメントを書く"
				fullWidth
				variant="standard"
				disabled={isCommentSending}
				onChange={(e) => setCommentText(e.target.value)}
			/>
      {/* 1つのボタンで画像を選択する */}
			<label htmlFor={inputId}>
				<Button
					variant="contained"
					disabled={images.length >= maxImagesUpload}
					component="span"
				>
					画像追加
				</Button>
				<input
					id={inputId}
					type="file"
					multiple
					accept="image/*,.png,.jpg,.jpeg,.gif"
					onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
						handleOnAddImage(e)
					}
					style={{ display: "none" }}
				/>
			</label>
      {/* 画像を選択したら選択中のすべての画像のプレビューを表示 */}
			{images.map((image, i) => (
				<div
					key={i}
					style={{
						position: "relative",
						width: "40%",
					}}
				>
					<IconButton
						aria-label="delete image"
						style={{
							position: "absolute",
							top: 10,
							left: 10,
							color: "#aaa",
						}}
						onClick={() => handleOnRemoveImage(i)}
					>
						<CancelIcon />
					</IconButton>
					<img
						src={URL.createObjectURL(image)}
						style={{
							width: "100%"
						}}
					/>
				</div>
			))}
      <br />
      <br />
			{isCommentSending ? (
				<CircularProgress />
			) : (
				<Button
					variant="contained"
					type="submit"
					disableElevation
					disabled={!commentText}
				>
					投稿
				</Button>
			)}
		</form>
	);
};
export default CommentForm;
reactで複数の画像をアップロードを実装するポイント
ご紹介したサンプルコードのポイントをご紹介します。
ポイントとなるのは以下のとおりです。
- 画像選択inputでmultipleを使う
 - inputに付与するidをランダムで生成
 - フォームで選択した画像ファイルのパスの取得
 - 選択した画像は配列で管理
 - 画像の送信はFormData()を使う
 
画像選択inputでmultipleを使う
サンプルコード内で以下のようにinputを書きました。
<input
  id={inputId}
  type="file"
  multiple
  accept="image/*,.png,.jpg,.jpeg,.gif"
  onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
    handleOnAddImage(e)
  }
  style={{ display: "none" }}
/>maltipleを付与したinputを使うことによって、inputが1つだけでreactが複数の画像を読み込むことができます。
画像選択inputに付与するidをランダムで生成
コンポーネント内でinputIdをランダムの文字列で生成し、それをlabelとinputに付与しています。
...
const inputId = Math.random().toString(32).substring(2);
...
<label htmlFor={inputId}>
  <Button
    variant="contained"
    disabled={images.length >= maxImagesUpload}
    component="span"
  >
    画像追加
  </Button>
  <input
    id={inputId}
    type="file"
    multiple
    accept="image/*,.png,.jpg,.jpeg,.gif"
    onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
      handleOnAddImage(e)
    }
    style={{ display: "none" }}
  />
</label>このようにランダムのidをコンポーネント内で生成することによって、1ページ内で複数フォームを設置した場合でも画像は任意のフォーム内で表示されます。
label内の書き方はmaterial UIのドキュメントを参考にしています。
https://mui.com/components/buttons/#upload-button
フォームで選択した画像ファイルのパスの取得
フォームで選択した画像ファイルのパスはURL.createObjectURL()を使うことで簡単に取得することができます。
<img
  src={URL.createObjectURL(image)}
  style={{
    width: "100%",
    borderRadius: "20px",
  }}
/>選択した画像は配列で管理
フォームで選択した複数の画像は配列で管理します。
inputでmulti属性を付与しているので、フォルダやスマホの写真ライブラリから1度に複数の写真をアップロードすることができます。
setImages([…images, …e.target.files]);で選択した複数の画像をまとめて配列に入れています。
  const [images, setImages] = useState<File[]>([]);
  ...
	const handleOnAddImage = (e: React.ChangeEvent<HTMLInputElement>) => {
		if (!e.target.files) return;
		setImages([...images, ...e.target.files]);
	};
	const handleOnRemoveImage = (index: number) => {
		const newImages = [...images];
		newImages.splice(index, 1);
		setImages(newImages);
	};
  ...画像ファイルが選択された時、handleOnAddImageで配列に追加します。
フォームで選択した画像を削除するときはhandleOnRemoveImageに配列のindexが渡されるので、そのindexを元に配列から指定の画像を消します。
画像の送信はFormData()を使う
画像の送信ではFormData()を使う必要があります。
FormData()に送信内容を詰め込んでaxiosでpostします。
	const handleOnSubmit = async (e: React.SyntheticEvent): Promise<void> => {
		e.preventDefault();
		setIsCommentSending(true);
		const target = e.target as typeof e.target & {
			comment: { value: string };
		};
		const data = new FormData();
		images.map((image) => {
			data.append("images[]", image);
		});
		data.append("comment", target.comment?.value || "");
		const postedComment = await axios.post(
			'/api/v1/comments',
			data
		);
    setIsCommentSending(false);
	};Reactとtypescriptの参考書をまとめておきます。

	






コメント