167
181

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【React / Next】ドラッグ&ドロップで画像を複数選択してDBにアップロードする

Last updated at Posted at 2022-08-02

個人の備忘録です。

環境

  • react: 18.2.0
  • next: 12.2.3
  • react-dropzone: 14.2.2
  • @chakra-ui/react: 2.2.4

やりたいこと

  • 複数の画像ファイルをページにドラッグ&ドロップする
  • 選択された画像をプレビュー表示する
  • プレビュー画像は削除可能
  • 選択された画像を DB へアップロードする
  • ライブラリは react-dropzone を使用

完成イメージ

Untitled.gif

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,
  };
};

モーダルパターン

  • モーダルを開いてから、ドラッグ&ドロップするパターン

完成イメージ

Untitled2.gif

サンプルコード

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 について

それぞれの特徴について

167
181
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
167
181

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?