Reactでファイル選択/アップロード/ローディング表示ができるボタンを作る(Material UI使用)
次の画像のようなUIのReactでの実装例です。
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の状態を変えられるようにしています。
具体的には、非同期処理onFileInputChange
をuseAsyncCallback
に渡して非同期通信の状態を取得できるようにして、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オブジェクトのカスタムメタデータを読み書きするなどが参考になるかと思います。