次の画像のような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オブジェクトのカスタムメタデータを読み書きするなどが参考になるかと思います。