React+Firebase Storageでファイルをドラッグしてアップロードする

# 概要

Reactで以下の内容を実装するサンプルです。ファイルは画像を前提にしています。

1.ファイルをドラッグするとファイルをプレビュー
2.登録ボタンを押すとstorageにアップロードし、最後にDB書き込み



## インストール

ファイルをドラッグしてファイル情報を取得するコンポーネントは `react-dropzone`を使います。

- react-dropzone
- https://react-dropzone.js.org/

firebaseもインストールします。

```
npm i -S react-dropzone
npm i -S firebase
```

オプショナルですがbootstrapを入れておきます

```
npm i -S bootstrap
```

## ドラッグでファイル情報を取得する

まず、ファイルをドラッグしたらファイル情報を取得するところを実装します。
onDropはドラッグしたときに呼ばれる関数で、callbackを利用しています。

```react
import React, { useState, useCallback } from 'react';
import { useDropzone } from 'react-dropzone';
import 'bootstrap/dist/css/bootstrap.min.css';

function App() {

    const [uploadfile, setUploadfile] = useState();
    const maxSize = 3 * 1024 * 1024;

    //dropzone
    const onDrop = useCallback((acceptedFiles) => {

        if (acceptedFiles.length > 0) {
            setUploadfile(acceptedFiles[0]);
        }

    }, []);

    //initialize
    const { acceptedFiles, getRootProps, getInputProps, isDragActive, isDragReject, fileRejections } = useDropzone({
        onDrop,
        accept: 'image/png, image/jpeg, image/gif, image/jpg',
        minSize: 1,
        maxSize,
    });

    return (

        <div className="container m-5 text-center">
            <div {...getRootProps()}>
                <input {...getInputProps()} />
                <p>ファイルをドラッグするかクリックしてください。</p>
                {uploadfile ? <p>選択されたファイル: {uploadfile.name}</p> : null}
            </div>
        </div>

    );
}

export default App;
```

ファイルがドラッグされるとコールバックでonDropが呼ばれ、acceptedFielsのなかに、ファイル情報が入ってきます。
これをuploadfileというステートに入れてファイル情報を画面に表示しています。

実行すると、以下のように選択されたファイルが表示されます。

<img width="761" alt="実行画面" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252067/7c939fc4-2df9-3008-884f-cf10a6e70594.png">


## 条件に一致しないファイルをエラーにする

initializeのところで、`maxSize`と`accept`を指定しています。
maxSizeはドロップできる最大サイズ、acceptはファイルタイプを指定しています。
これを指定することで自動的にファイルをリジェクトしてくれます。

ドラッグすると、ファイルタイプが一致しない場合は`isDragReject`が`true`、ファイルサイズではねられるものは`fileRejections`にエラー情報が入ってきます。
`isDragReject` はドラッグ中のみ取得できます。

```react

        <div className="container m-5 text-center">
            <div {...getRootProps()}>
                <input {...getInputProps()} />
                <p>ファイルをドラッグするかクリックしてください。</p>
                {uploadfile ? <p>選択されたファイル: {uploadfile.name}</p> : null}
                {isDragReject ? <div className="alert alert-danger" role="alert">ファイルタイプが一致しません</div> : null}
                {fileRejections.length > 0 ? <div className="alert alert-danger" role="alert">
                    {fileRejections[0].errors[0].message}
                </div> : null}
            </div>
        </div>
```

`fireRejections`に入っている`errors`がエラー情報です。codeとmessageが英語で入っているので、実際の実装時にはcodeをみてメッセージを表示すると良いと思います。

<img width="891" alt="スクリーンショット 2020-05-14 7.05.43.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252067/ce810108-877d-9ba4-4e79-8933392c928a.png">


## ドラッグした時にプレビュー表示する

次にプレビュー表示します。
画像を表示するためにファイル情報をURL形式にし、state(fileUrl)に設定します。

stateの設定とonDrop を次ように変更します。

```react

    const [fileUrl, setFileUrl] = useState();

//省略

    const onDrop = useCallback((acceptedFiles) => {

        if (acceptedFiles.length > 0) {
            const src = URL.createObjectURL(acceptedFiles[0]);
            setFileUrl(src);
            setUploadFile(acceptedFiles[0]);
        }

    }, []);
```

取得した画像を表示する部分です

```diff
        <div className="container m-5 text-center">
            <div {...getRootProps()}>
                <input {...getInputProps()} />
                <p>ファイルをドラッグするかクリックしてください。</p>
                {uploadfile ? <p>選択されたファイル: {uploadfile.name}</p> : null}
                {isDragReject ? <div className="alert alert-danger" role="alert">ファイルタイプが一致しません</div> : null}
                {fileRejections.length > 0 ? <div className="alert alert-danger" role="alert">
                    {fileRejections[0].errors[0].message}
                </div> : null}
+               <div className="card mt-2" style={{ margin: 'auto', width: '200px', height: '200px' }} >
+                   <img src={fileUrl} width="100%" />
+               </div>
            </div>
        </div>
```

実行したところです。

<img width="595" alt="スクリーンショット 2020-05-14 9.30.25.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252067/e02763c7-8fbd-b3e4-9218-18e5da4e52a8.png">


## storageにアップロードしてDBに書き込む

次にstroageにアップロードして、参照用のdownloadUrlを取得してDBに書きこみます。
firebase用の設定は割愛します。
作成したfirebaseの設定をimportで予めimportしておきます。

登録部分を実装します。

```react
//省略
import firebase from './firebase'; //firebaseの設定

function App() {

//省略

    const [message, setMessage] = useState();

    //submit
    const upload = async () => {

        //fire upload
        let url = "";
        if (uploadfile.name) {
            const storageref = firebase.storage().ref('sample/' + uploadfile.name);
            const snapshot = await storageref.put(uploadfile);
            url = await snapshot.ref.getDownloadURL();
        }

        //db updated
        if (url) {
            await firebase.firestore().collection('sample').doc().set({
                filename: uploadfile.name,
                fileUrl: url,
            });

            setMessage('登録しました');
        }
    }


```

先にstorageにアップし、アップした時にgetDownloadUrlで参照用のstorageのURLを取得します。
それをfirestoreに登録しています。

登録ボタンと登録後のメッセージ表示の部分です。

```diff

        <div className="container m-5 text-center">
            <div {...getRootProps()}>
                <input {...getInputProps()} />
                <p>ファイルをドラッグするかクリックしてください。</p>
                {uploadfile ? <p>選択されたファイル: {uploadfile.name}</p> : null}
                {isDragReject ? <div className="alert alert-danger" role="alert">ファイルタイプが一致しません</div> : null}
                {fileRejections.length > 0 ? <div className="alert alert-danger" role="alert">
                    {fileRejections[0].errors[0].message}
                </div> : null}

                <div className="card mt-2" style={{ margin: 'auto', width: '200px', height: '200px' }} >
                    <img src={fileUrl} width="100%" />
                </div>
            </div>
+            <button type="button" className="btn btn-primary mt-2" onClick={upload}>登録</button>
+            {message ? <div className="alert alert-success mt-2" role="alert">{message}</div> : null}
        </div>

```

実行してみた画面です。

<img width="768" alt="スクリーンショット 2020-05-14 10.11.07.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252067/1667be6f-f8bf-c765-6829-4d3a6b7534ea.png">

storageにもfirestoreにも書き込まれています。

storage
<img width="868" alt="スクリーンショット 2020-05-14 10.09.53.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252067/b3572ee1-4263-bc5e-584c-0f07ca9b7f88.png">

firestore
<img width="752" alt="スクリーンショット 2020-05-14 10.08.38.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252067/4c1a573f-448b-aef5-89ac-e46211a8c964.png">

以上です。