個人の備忘録です。
環境
- react: 18.2.0
- next: 12.2.3
- react-dropzone: 14.2.2
- @chakra-ui/react: 2.2.4
やりたいこと
- 複数の画像ファイルをページにドラッグ&ドロップする
- 選択された画像をプレビュー表示する
- プレビュー画像は削除可能
- 選択された画像を DB へアップロードする
- ライブラリは
react-dropzone
を使用
完成イメージ
install
- まずは、
react-dropzone
を install
yarn add react-dropzone
サンプルコード
Contents.tsx
import { Img } from '@chakra-ui/react';
import { useState } from 'react';
import { useDropzone } from 'react-dropzone';
import { useImageNew } from 'usecase/useImageNew';
import styles from './contents.module.scss';
export const Contents = () => {
// ブラウザ表示用の paths
const [previewImagePaths, setPreviewImagePaths] = useState<string[]>();
// upload用の files
const [files, setFiles] = useState<File[]>();
const { imageNewHandler } = useImageNew();
//*********************
/** 選択された画像を処理 */
//*********************
const onDrop = (acceptedFiles: File[]) => {
// 引数で受け取れる値は、File型の配列なので upload用のstateへsetする
setFiles(acceptedFiles);
// ブラウザで画像を表示させるための、一時的なURLをメモリに生成する
// @see https://developer.mozilla.org/ja/docs/Web/API/URL/createObjectURL
const dataUrls = acceptedFiles.map((file) => URL.createObjectURL(file));
// createObjectURLで生成された、ブラウザ表示用のURLをstateへsetする
setPreviewImagePaths(dataUrls);
};
const { getRootProps, getInputProps } = useDropzone({ onDrop });
//************************************************
/** 削除ボタンをクリックすると、プレビュー画像を削除する */
//************************************************
const handleClickDelete = () => {
setPreviewImagePaths([]);
};
//*********************************
/** アップロードボタンクリック時の処理 */
//*********************************
const handleClickUpload = async () => {
const formData = buildFormData(files);
const response = await imageNewHandler(formData);
// 省略
};
const buildFormData = (files?: File[]) => {
if (!files) {
return new FormData();
}
// DB へアップロードするために、FormData へ append する
// @see https://developer.mozilla.org/ja/docs/Web/API/FormData/Using_FormData_Objects
const formData = new FormData();
// append の第一引数はバックエンドと合わせる
files.forEach((file) => formData.append('images', file, file.name));
return formData;
};
return (
<>
<div className={styles.contents}>
<div {...getRootProps({ className: `dropzone ${styles.contents__input}` })}>
<input {...getInputProps()} />
<p>
Drag and drop some image files here, or click to select image files
</p>
</div>
</div>
{previewImagePaths &&
previewImagePaths.map((image, i) => (
<div key={i}>
<Img src={image} />
</div>
))}
<div className={styles.contents__button}>
<button onClick={handleClickDelete}>Delete preview images</button>
</div>
<div className={styles.contents__button}>
<button onClick={handleClickUpload}>Upload images</button>
</div>
</>
);
};
contents.module.scss
.contents {
margin: 0 auto;
border: 2px dashed #ccc;
background-color: #eee;
color: #bbb;
max-width: 600px;
height: 100px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
&__input {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
&__button {
margin-top: 20px;
}
}
useImageNew.ts
import { ImagePostResponse } from 'entity/dropzone/imagePost';
import baseHttpClient from 'infrastructure/httpClient';
// NOTE: 簡易的なレイヤー
export const useImageNew = () => {
const imageNewHandler = async (formData: FormData) => {
const response = await baseHttpClient.post<ImagePostResponse>(
`/api/image`,
formData,
{
// content-type を multipart/form-data に指定
// @see https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Content-Type
// @see https://developer.mozilla.org/ja/docs/Web/API/FormData/Using_FormData_Objects
headers: {
'content-type': 'multipart/form-data',
},
},
);
return response?.data;
};
return {
imageNewHandler,
};
};
モーダルパターン
- モーダルを開いてから、ドラッグ&ドロップするパターン
完成イメージ
サンプルコード
contents.tsx
import { Img, useDisclosure } from '@chakra-ui/react';
import { useState } from 'react';
import { useDropzone } from 'react-dropzone';
import { useImageNew } from 'usecase/useImageNew';
import { ImageModal } from '../modal/modal';
import styles from './contents.module.scss';
export const Contents = () => {
const { isOpen, onOpen, onClose } = useDisclosure();
// ブラウザ表示用の paths
const [previewImagePaths, setPreviewImagePaths] = useState<string[]>();
// upload用の files
const [files, setFiles] = useState<File[]>();
const { imageNewHandler } = useImageNew();
//*********************
/** 選択された画像を処理 */
//*********************
const selectImages = (acceptedFiles: File[]) => {
setFiles(acceptedFiles);
const dataUrls = acceptedFiles.map((file) => URL.createObjectURL(file));
setPreviewImagePaths(dataUrls);
};
const handleClickDelete = () => {
setPreviewImagePaths([]);
};
const handleClickUpload = async () => {
const formData = buildFormData(files);
const response = await imageNewHandler(formData);
};
const buildFormData = (files?: File[]) => {
if (!files) {
return new FormData();
}
// DB へアップロードするために、FormData へ append する
const formData = new FormData();
// append の第一引数はバックエンドと合わせる
files.forEach((file) => formData.append('images', file, file.name));
return formData;
};
const handleClickModal = () => {
onOpen();
};
return (
<div className={styles.contents}>
{previewImagePaths &&
previewImagePaths.map((image, i) => (
<div key={i}>
<Img src={image} />
</div>
))}
<ImageModal
isOpen={isOpen}
onClose={onClose}
selectImages={selectImages}
/>
<div className={styles.contents__button}>
<button onClick={handleClickDelete}>Delete preview images</button>
</div>
<div className={styles.contents__button}>
<button onClick={handleClickUpload}>Upload images</button>
</div>
<div className={styles.contents__button}>
<button onClick={handleClickModal}>Open modal</button>
</div>
</div>
);
};
modal.tsx
import {
Box,
Modal,
ModalBody,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
} from '@chakra-ui/react';
import { useDropzone } from 'react-dropzone';
import styles from './modal.module.scss';
type Props = {
isOpen: boolean;
onClose: () => void;
selectImages: (acceptedFiles: File[]) => void;
};
export const ImageModal = ({ isOpen, onClose, selectImages }: Props) => {
//************************************
/** 選択された画像を処理するイベントを通知 */
//************************************
const onDrop = (acceptedFiles: File[]) => {
selectImages(acceptedFiles);
};
const { getRootProps, getInputProps } = useDropzone({ onDrop });
const handleClickOk = () => {
onClose();
};
return (
<Modal isOpen={isOpen} onClose={onClose} isCentered size={'2xl'}>
<ModalOverlay />
<ModalContent>
<ModalHeader className={styles.modal__header}>画像の選択</ModalHeader>
<ModalBody className={styles.modal__body}>
<div className={styles.modal__contents}>
<div {...getRootProps({ className: `dropzone ${styles.modal__input}` })}>
<input {...getInputProps()} />
<p>
Drag and drop some image files here, or click to select image
files
</p>
</div>
</div>
</ModalBody>
<ModalFooter>
<Box className={styles.modal__footer}>
<button onClick={handleClickOk}>OK</button>
</Box>
</ModalFooter>
</ModalContent>
</Modal>
);
};
参考
react-dropzoneについて
multipart/form-data について
FormData について
FileReader について
File / Blob について
Base64 について
それぞれの特徴について