Reactでファイル選択/アップロード/ローディング表示ができるボタンを作る(Material UI使用)

ReactでMaterial UIを使って、ちょっとオシャレなファイルアップロードボタンを実装してみました。
2022.05.20

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

次の画像のようなUIのReactでの実装例です。

ファイルアップロードUI動作イメージ

Material UIのProgressにある「Interactive integration」を実際に動作させるための実装例になります。

特徴

特徴は次の通りです。

  • デフォルトのファイル選択UIは非表示
  • ボタンクリックで、ファイル選択UIが開く
  • ファイルを選択したら処理が始まり、ボタンのラベルが処理中の表示になる
  • 処理が終わったらボタンの色がグリーンになって処理が成功したことを表現する

サンプルコード

FileUploadUIコンポーネントの実装は次の通りです。

import React, { useState, useRef } from 'react'
import { useAsyncCallback } from 'react-async-hook'
import Box from '@mui/material/Box'
import CircularIntegration from './circularintegration.js';

const initialState = {
  file: null,
}

const FileUploadUI = () => {
  const inputRef = useRef(null)
  const [formState, setFormState] = useState(initialState)
  const [success, setSuccess] = useState(false)

  const uploadFile = async(file) => {
    if (!file) return

    /* アップロード処理に見立てた時間のかかる処理 */
    const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
    await sleep(5000)

    /* アップロード処理が成功したらフォームの状態を
       初期化してsuccessステートをtrueにする */
    setFormState(initialState)
    setSuccess(true)
  }

  const onFileInputChange = async (event) => {
    const file = event.target.files[0]
    await uploadFile(file)
  }

  const clickFileUploadButton = () => {
    setSuccess(false)
    inputRef.current.click()
  }

  const asyncEvent = useAsyncCallback(onFileInputChange);

  return (
    <Box>
      <CircularIntegration
        onClick={clickFileUploadButton}
        asyncEvent={asyncEvent}
        success={success}
        component="label"
        text={asyncEvent.loading ? '...' : "Upload File"}
      />
      <input
        hidden
        ref={inputRef}
        type="file"
        onChange={asyncEvent.execute}
      />
    </Box>
  )
}

export default FileUploadUI

React-Async-Hookを使って、ファイル選択後からファイルアップロードまでの非同期処理の状態によってUIの状態を変えられるようにしています。 具体的には、非同期処理onFileInputChangeuseAsyncCallbackに渡して非同期通信の状態を取得できるようにして、CircularIntegrationに渡しています。

CircularIntegrationの実装は次の通りです。

import React from 'react'
import Box from '@mui/material/Box'
import CircularProgress from '@mui/material/CircularProgress'
import { green } from '@mui/material/colors'
import Button from '@mui/material/Button'

export default function CircularIntegration(props) {
  const { asyncEvent, success, onClick } = props

  const buttonSx = {
    ...(success && {
      bgcolor: green[500],
      '&:hover': {
        bgcolor: green[700],
      },
    }),
  }

  return (
    <Box sx={{ display: 'flex', alignItems: 'center' }}>
      <Box sx={{ m: 1, position: 'relative' }}>
        <Button
          variant="contained"
          sx={buttonSx}
          disabled={asyncEvent.loading}
          onClick={() => {
            if (onClick) {
              onClick()
            }
            asyncEvent.execute()
          }}
        >
          {props.text}
        </Button>
        {asyncEvent.loading && (
          <CircularProgress
            size={24}
            sx={{
              color: green[500],
              position: 'absolute',
              top: '50%',
              left: '50%',
              marginTop: '-12px',
              marginLeft: '-12px',
            }}
          />
        )}
      </Box>
    </Box>
  )
}

CircularIntegrationでは、受け取ったasyncEventを使って、asyncEvent.execute()で非同期処理を開始し、asyncEvent.loadingで非同期処理が実行中かどうかを判定しています。

ファイルを実際にS3などにアップロードする処理の実装に関しては、AWS Amplify + ReactでS3オブジェクトのカスタムメタデータを読み書きするなどが参考になるかと思います。