reactで複数の画像をアップロードする

React
language

この記事では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;
		const img: File = e.target.files[0];
		setImages([...images, img]);
	};

	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%",
							borderRadius: "20px",
						}}
					/>
				</div>
			))}
			{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のドキュメントを参考にしています。

React Button component - MUI
Buttons allow users to take actions, and make choices, with a single tap.

フォームで選択した画像ファイルのパスの取得

フォームで選択した画像ファイルのパスはURL.createObjectURL()を使うことで簡単に取得することができます。

<img
  src={URL.createObjectURL(image)}
  style={{
    width: "100%",
    borderRadius: "20px",
  }}
/>

選択した画像は配列で管理

フォームで選択した複数の画像は配列で管理します。

  const [images, setImages] = useState<File[]>([]);
  ...

	const handleOnAddImage = (e: React.ChangeEvent<HTMLInputElement>) => {
		if (!e.target.files) return;
		const img: File = e.target.files[0];
		setImages([...images, img]);
	};

	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);
	};
タイトルとURLをコピーしました