logo
Search
Programming

React × DRFで画像アップロード機能作成 (フロント実装編)

#React #Django REST Framework
Oct 26th 2021 Oct 27th 2021
React × DRFで画像アップロード機能作成 (フロント実装編)

前回の記事で、開発環境の構築が完了しました。今回は、フロントエンド側の実装に入りたいと思います。

前回までの記事はこちらです。

実行環境

  • Mac OS X 11.2.1
  • Docker 20.10.5
  • docker-compose version 1.28.5
  • node v15.10.0
  • npm 7.9.0
  • react 17.2.0

コンテナに入る

前回作成したclient用のコンテナが起動していること確認後、下記コマンドを実行して、client用のコンテナに入ります。

$ docker exec -it client sh

Reactアプリの作成

コンテナに入ったら、下記のコマンドを実行してReactのアプリを作成します。

$ cd client
$ npx create-react-app --template typescript

コマンドを実行すると、各ライブラリのインストールが始まります。インストールが完了すると、下記メッセージが出力されます。

We suggest that you begin by typing:

  cd img_uploader
  yarn start

Happy hacking!

開発用サーバを起動

アプリの作成が無事に完了しているか確認するために、一度開発用サーバを起動して確認します。

$ cd img_uploader
$ yarn start

サーバを起動したら、http://localhost:3000/に接続します。下記画面が表示されていれば、アプリの作成とサーバに起動は成功です。

React初期画面

ライブラリのインストール

次に、アプリ作成に必要なライブラリを別途インストールしていきます。

今回インストールするライブラリは下記になります。

  • axios
  • Material-UI
  • react-dropzone

下記コマンドを実行して、ライブラリをインストールします。

$ yarn add axios @material-ui/core react-dropzone
$ yarn add -D @types/react-dropzone

機能概要

ライブラリのインストールが完了したら、早速フロント側の機能実装に移りたいと思います。今回作成するアプリの機能は以下のようなものになります。

  • ドラッグアンドドロップで画像選択
  • 「Selectボタン」でファイル選択
  • 選択された画像のプレビュー
  • 「Uploadボタン」でサーバに選択された画像を送信

実装

早速、実装に移ろうと思います。まずは、画面を描画しているApp.tsxの編集を行っていきます。

必要ライブラリのインポート

まずは、今回使用するライブラリをインポートしていきます。

import React, { useState, useCallback } from 'react'
import { makeStyles } from '@material-ui/core/styles'
import {
    Container,
    Card,
    CardContent,
    CardActions,
    Typography,
    Button,
} from '@material-ui/core'
import { useDropzone } from 'react-dropzone'
import './App.css';
import bg from './assets/image/upload_bg.jpg'
import axios from 'axios'

Material UIのデザインアレンジ

次に、前回インストールした画面のデザインを簡単に作ってくれるライブラリであるMaterial UIのデザインを少しアレンジしていきます。

Material UIの詳しい使用方法は、公式ドキュメントを参照してください。

const useStyles = makeStyles({
    root: {
        minWidth: 300,
        position: 'absolute',
        top: '50%',
        left: '50%',
        transform: 'translate(-50%, -50%)'
    },
    title: {
        textAlign: 'center',
    },
    subtitle: {
        textAlign: 'center',
    },
    actions: {
        justifyContent: 'center',
    }
})

コンポーネントの作成

実際に描画されるコンポーネントの作成を行っていきます。

// Dropzoneの設定
const acceptFile = 'image/*'

type MyFile = File & {
    preview: string
}

const App:React.FC = () => {

    // Style
    const classes = useStyles()

    // State
    const [isShow, setIsShow] = useState(false)
    const [files, setFiles] = useState<MyFile[]>([])

    // ドロップした時の処理
    const onDrop = useCallback((acceptedFiles: File[]) => {
        setIsShow(true)

        setFiles(acceptedFiles.map(
            file => Object.assign(file, {
                preview: URL.createObjectURL(file)
            })
        ))
    }, [])

    // Dropzone
    const { getRootProps, getInputProps, isDragActive, open }
        = useDropzone({ noClick: true, onDrop, accept: acceptFile })

    const upload = () => {
        const data = new FormData()
        files.forEach(file => data.append('file', file))
        axios.post('http://localhost:8000/api/image/', data)
        .then(res => {
            console.log(res)
            setIsShow(false)
            setFiles([])
        })
        .catch(e => {
            console.log(e)
        })
    }

    return (
        <Container maxWidth="sm">
            <Card className={classes.root}>
                <CardContent>
                    <Typography variant='h6' component='h2' className={classes.title}>
                        Image uploader
                    </Typography>
                    <Typography component='p' color='textSecondary' className={classes.subtitle}>
                        drag & drop
                    </Typography>
                    <div {...getRootProps({ className: 'dropzone' })}>
                        {isShow ? (
                            files.map(file => (
                                <img key={file.name} src={file.preview} alt={file.name} width='200' />
                            ))
                        ) : (
                            <img src={upload_bg} alt='bg' className={'upload_img ' + (isDragActive ? 'is-on' : '') } width='200' />
                        )}
                        <input {...getInputProps()} />
                    </div>

                </CardContent>

                <CardActions className={classes.actions}>
                    <Button
                        size='small'
                        color='primary'
                        variant='contained'
                        component="span"
                        onClick={open}
                    >Select</Button>
                    {isShow && (
                        <Button
                            size='small'
                            color='primary'
                            variant='contained'
                            component='span'
                            onClick={upload}
                        >Upload</Button>
                    )}
                </CardActions>
            </Card>
        </Container>
    )
}

export default App;

useStyles()

const classes = useStyles()

ここでは、上で定義したMaterial UIのデザインのアレンジするための関数を実行して、その戻り値をclassesに格納することで、デザインを適用するための処理です。

実際に、デザインを適用するためには、下記のようにclassNameに適用させたいクラス名を定義します。

<Card className={classes.root}>

usState()

useState関数を使用して、コンポーネント内での状態管理を行います。useStateは下記のようにして使用します。

const [変数, set変数] = useState(初期値)

今回は、画像が選択された際に、画面の表示を切り替えるためのフラグをここで管理しています。

const [isShow, setIsShow] = useState(false)
const [files, setFiles] = useState<MyFile[]>([])// ドロップした時の処理
    const onDrop = useCallback((acceptedFiles: File[]) => {
        setIsShow(true)

        setFiles(acceptedFiles.map(
            file => Object.assign(file, {
                preview: URL.createObjectURL(file)
            })
        ))
    }, [])

// ...

// isShowがFalseなら、デフォルトで表示するべき画像を表示しています。
{isShow ? (
    files.map(file => (
        <img key={file.name} src={file.preview} alt={file.name} width='200' />
    ))
) : (
    <img src={upload_bg} alt='bg' className={'upload_img ' + (isDragActive ? 'is-on' : '') } width='200' />
)}

ドラッグアンドドロップの処理

react-dropzoneというライブラリを使用して、コンポーネントの一部のエリアにドラッグアンドドロップをすることによって、画像を選択させるといった処理が可能になります。


// ドロップした時の処理
const onDrop = useCallback((acceptedFiles: File[]) => {
		// 
    setIsShow(true)

    setFiles(acceptedFiles.map(
        file => Object.assign(file, {
            preview: URL.createObjectURL(file)
        })
    ))
}, [])

// useDropzone実行時に渡しているプロパティについて
// noClick: trueを渡すことで、ドラッグアンドドロップでのみ画像選択を許可する
// onDropを渡すことで、ドラッグアンドドロップ時に、実行する関数を指定できる
// Dropzone
const { getRootProps, getInputProps, isDragActive, open }
    = useDropzone({ noClick: true, onDrop, accept: acceptFile })

// ...

<div {...getRootProps({ className: 'dropzone' })}>
    {isShow ? (
        files.map(file => (
            <img key={file.name} src={file.preview} alt={file.name} width='200' />
        ))
    ) : (
        <img src={upload_bg} alt='bg' className={'upload_img ' + (isDragActive ? 'is-on' : '') } width='200' />
    )}
    <input {...getInputProps()} />
</div>

API呼び出し

Uploadボタンを押した時に、このあと作成するAPIを呼び出すための処理の実装を見ていきます。

{isShow && (
  <Button
      size='small'
      color='primary'
      variant='contained'
      component='span'
      onClick={upload}
  >Upload</Button>
)}

const upload = () => {
    const data = new FormData()
    files.forEach(file => data.append('file', file))
    axios.post('http://localhost:8000/api/image/', data)
    .then(res => {
				// API成功時の処理
        console.log(res)
        setIsShow(false)
        setFiles([])
    })
    .catch(e => {
        console.log(e)
    })
}

uploadボタンは、isShowフラグがtrueのときのみ表示され、ボタンを押下した時にupload関数が実行されます。

upload関数では、選択されたファイルをFormDataに格納していき、それをAxiosを使用して、APIを実行しています。

APIの処理が成功したときは、.then の処理に入りisShowフラグを折り、選択ファイルを空にしています。

デザインを整える

App.tsxの実装が完了したら次は、CSSで細かいデザインを整えて行きます。

body {
    background-color: #eee;
}

.upload_img {
    display: block;
    margin: 20px auto;
    border-style: dashed;
    border-radius: 10px;
    border-color: #7c88cb;
}
.upload_img.is-on { opacity: .7; }
.hidden { display: none; }

img {
    display: block;
    margin: 20px auto;
}

動作確認

ここまで実装が済んだら、実際に画像をドラッグアンドドロップしてみます。
img-uploader-drug and drop

選択した画像がプレビューされることが確認でき、Uploadボタンが表示されたら一先ずOKです。
img-uploader-preview

まとめ

今回は、ドラッグアンドドロップで画像を選択して、選択された画像をプレビューするところまで実装しました。次回は、Uploadボタンを押下時に受け取るサーバ側の処理を実装していこうと思います。

Comments