この記事では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の参考書をまとめておきます。
コメント