MUI ( Nextjs / Material-UI ) で画像アップロードダイアログを作成し、プレビューする。

MUIを利用して、画像アップロードコンポーネントを作成します。

  • 画像アップロードボタンをクリックすると、input要素標準の画像選択ダイアログを表示
  • 単一、もしくは複数の選択した画像をプレビュー表示
  • 画像は3カラムで表示
  • 右上の×ボタンをクリックすると、プレビュー表示から削除

この画像アップロードコンポーネントで設定された画像データをPOSTすることでサーバーに保存することができるようになります。
本記事ではブラウザ側のメモリにファイルを保存し、プレビュー表示するまでを記載します。

目次

ソース全体

結論、下記の形で実装が可能です。

import React from 'react'
import Button from '@mui/material/Button'
import IconButton from '@mui/material/IconButton'
import CancelIcon from '@mui/icons-material/Cancel'
import { Grid } from '@mui/material'

type Props = {
  images: File[]
  setImages: (arg: File[]) => void
}

const FileUploader = (props: Props) => {
  const maxImagesUpload = 10
  const inputId = Math.random().toString(32).substring(2)

  const handleOnAddImage = async (e: React.ChangeEvent<HTMLInputElement>) => {
    if (!e.target.files) return
    const files: File[] = []

    for (const file of e.target.files) {
      files.push(file)
    }

    props.setImages([...props.images, ...files])
    e.target.value = ''
  }

  const handleOnRemoveImage = (index: number) => {
    const newImages = [...props.images]
    newImages.splice(index, 1)
    props.setImages(newImages)
  }

  return (
    <>
      <Grid container spacing={{ xs: 2, md: 3 }} columns={{ xs: 8, sm: 12, md: 12 }}>
        {props.images.map((image, i) => (
          <Grid
            item
            xs={4}
            sm={4}
            md={4}
            key={i}
            sx={{
              display: 'flex',
              justifyContent: 'start',
              alignItems: 'center',
              position: 'relative'
            }}
          >
            <IconButton
              aria-label='delete image'
              style={{
                position: 'absolute',
                top: 10,
                right: 0,
                color: '#aaa'
              }}
              onClick={() => handleOnRemoveImage(i)}
            >
              <CancelIcon />
            </IconButton>
            <img
              src={URL.createObjectURL(image)}
              style={{
                width: '100%',
                height: '100%',
                objectFit: 'contain',
                aspectRatio: '1 / 1'
              }}
              alt=''
            />
          </Grid>
        ))}
      </Grid>
      <label htmlFor={inputId}>
        <Button variant='contained' disabled={props.images.length >= maxImagesUpload} component='span' sx={{ mt: 4 }}>
          画像アップロード
        </Button>
        <input
          id={inputId}
          type='file'
          multiple
          accept='image/*,.png,.jpg,.jpeg,.gif'
          onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleOnAddImage(e)}
          style={{ display: 'none' }}
        />
      </label>
    </>
  )
}

export default FileUploader

親コンポーネントでuseState<File[]>を定義して、FileUploaderコンポーネントへ渡します。
FileUploaderコンポーネント側で追加されたFileを親コンポーネント側でセットすることで画像のプレビューを行います。

const [images, setImages] = useState<File[]>([])
<FileUploader
  images={images}
  setImages={setImages}
></FileUploader>

解説

「画像アップロードボタンをクリックすると、input要素標準の画像選択ダイアログを表示」に関しては、input要素のtypeに’file’を指定するだけです。
multiple属性をつけることで複数のファイルを選択できます。
さらにacceptで選択できるファイル拡張子を絞り込んでいます。

選択時に、handleOnAddImage関数を呼び出し、setImagesで画像をメモリに登録します。

      <label htmlFor={inputId}>
        <Button variant='contained' disabled={props.images.length >= maxImagesUpload} component='span' sx={{ mt: 4 }}>
          画像アップロード
        </Button>
        <input
          id={inputId}
          type='file'
          multiple
          accept='image/*,.png,.jpg,.jpeg,.gif'
          onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleOnAddImage(e)}
          style={{ display: 'none' }}
        />
      </label>

setImagesに画像を登録すると、コンポーネントが再描画されるので、mapでまわし、URL.createObjectURLで画像を表示しています。

{props.images.map((image, i) => (
  <img src={URL.createObjectURL(image)}/>
))}

「右上の×ボタンをクリックすると、プレビュー表示から削除」では、handleOnRemoveImageでクリックされた画像のIndexを渡してimagesのメモリ上から削除し、setimagesしなおしています。

まとめ

MUIを利用するとデザインは簡易になるので、ロジック面だけに集中できるのはメリットです。
画像アップロードもFileインターフェースの扱いが戸惑いそうなところですが、MDNを読むとすんなりと理解が進みそうです。

よかったらシェアしてね!
目次