LoginSignup
50
27

More than 1 year has passed since last update.

React + react-dropzone: ファイルをページにドラッグ&ドロップする

Last updated at Posted at 2021-04-18

react-dropzone

react-dropzoneとは

react-dropzoneは、ローカルのファイルをドラッグ&ドロップやダイアログで選んで扱うためのライブラリです。ファイルをアップロードするユーザーインタフェースなどに使えます。アップデートは小まめで、本稿執筆時のバージョンはv11.3.2です。最小限のコード例はとても短く、APIにはフックも用いられています(サンプル001)。

サンプル001■最小限のコード例

2104001_001.png
>> CodeSandboxへ

インストール

コマンドラインツールで、npmやyarnを使ってインストールしてください。

npm install --save react-dropzone
yarn add react-dropzone

本稿の作例は、Create React Appのひな形アプリケーションをもとにつくっています。ひな形のつくり方については「Reactアプリケーションのひな形をつくる」をお読みください。

コード例を試す

使い方(Usage)に掲げられているコード例は、わずか20行足らずです。それでも、コードをコピー&ペーストすれば、最小限のユーザーインタフェースができ上がります。

それに少しだけ、スタイルなどの手を加えたのがつぎのコード001です。コードはたしかに短い。でも、ちょっと何やってるかわからないですね。まずは、冒頭のサンプル001のCodeSandboxコード例で、どういう動きになるのかをご覧ください。

コード001■最小限のコード例

src/App.js
import { useCallback } from 'react';
import { useDropzone } from 'react-dropzone';

const style = {
    width: 200,
    height: 150,
    border: "1px dotted #888"
};
function App() {
    const onDrop = useCallback((acceptedFiles) => {
        // Do something with the files
        console.log('acceptedFiles:', acceptedFiles);
    }, []);
    const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop });
    return (
        <div {...getRootProps()} style={style}>
            <input {...getInputProps()} />
            {
                isDragActive ?
                    <p>Drop the files here ...</p> :
                    <p>Drag 'n' drop some files here, or click to select files</p>
            }
        </div>
    );
}

export default App;

フックuseDropzoneに引数として{ onDrop }が与えられています。すると、ページ内の領域(<div>要素)にファイルをドロップしたとき、コンポーネントのonDropに定めたコールバックが呼び出されます。コールバックに渡される引数は、ドロップしたFileオブジェクトが収められたFileListです。ここでは、コンソールに出力して中身を確かめています。

useDropzoneから取り出したisDragActiveは、領域にファイルがドラッグされているかどうかを調べる論理値です。前掲コード001では、ドラッグすると表示されるテキスト(p要素)が切り替わります。

getRootProps()の戻り値をコンソールに出力してみました。中身にはつぎのようなイベントハンドラが含まれています。これらがページの領域に加えられ、その中のひとつonDropがコンポーネントに定めたコールバックを呼び出したのです。

onBlur: ƒ (event)
onClick: ƒ (event)
onDragEnter: ƒ (event)
onDragLeave: ƒ (event)
onDragOver: ƒ (event)
onDrop: ƒ (event)
onFocus: ƒ (event)
onKeyDown: ƒ (event)
ref: {current: null}
tabIndex: 0

getInputProps()の戻り値は、つぎのとおりでした。<input>要素の属性とイベントハンドラが含まれているようです。

accept: undefined
autoComplete: "off"
multiple: true
onChange: ƒ (event)
onClick: ƒ (event)
ref: {current: null}
style: {display: "none"}
tabIndex: -1
type: "file"

スタイルを定める

前掲コード001(サンプル001)では、ページの領域(<div>要素)のスタイルをオブジェクトにしてReactおなじみのstyleプロパティに与えました。同じことは、getRootProps()の引数オブジェクトにstyleプロパティとして渡しても実現できます。

src/App.js
function App() {

    return (
        // <div {...getRootProps()} style={style}>
        <div {...getRootProps({ style })}>

        </div>
    );
}

ページ内の領域のスタイルは、ドラッグしたとき動的に変えてみましょう。ドラッグしているかどうかは、isDragActiveで調べられました。領域にドラッグしたときのスタイル(borderDragStyle)には、軽くアニメーション(transition)も加えています。なお、useMemoを用いたメモ化については、「Create React App 入門 08: useMemoフックで無駄な再計算を省く」をお読みください。

src/App.js
// import { useCallback } from 'react';
import { useCallback, useMemo } from 'react';

// const style = {
const baseStyle = {

    // border: "1px dotted #888"
};
const borderNormalStyle = {
    border: "1px dotted #888"
};
const borderDragStyle = {
    border: "1px solid #00f",
    transition: 'border .5s ease-in-out'
};
function App() {

    const style = useMemo(() => (
        { ...baseStyle, ...(isDragActive ? borderDragStyle : borderNormalStyle)}
    ), [isDragActive]);

}

これで、ファイルをドラッグすると領域の枠線スタイルが動的に変わります(サンプル002)。

サンプル002■ドラッグした領域のスタイルが動的に変わる

2104001_002.png
>> CodeSandboxへ

ダイアログはボタンで開く

ドラッグ&ドロップはいいとして、ダイアログを開くのはボタンの方がわかりやすそうです。useDropzone()の引数に{ noClick: true }を渡すことにより領域クリックでダイアログが開くのは止め、開くための関数openを取り出します。そして、ボタン(<button>要素)のonClickハンドラからopenを呼び出すようにしたのがつぎのコードです。

src/App.js
function App() {

    // const { getRootProps, getInputProps, isDragActive } = useDropzone({
    const { getRootProps, getInputProps, isDragActive, open } = useDropzone({

        noClick: true
    });

    return (
        <div {...getRootProps({ style })}>

            <button type="button" onClick={open}>Select files</button>
        </div>
    );
}

ドロップしたファイルの情報を得る

ドロップしたファイルのFileListオブジェクトは、useDropzoneフックからacceptedFilesとして得られます。この中から取り出したFileオブジェクトそれぞれのファイル名(path)とサイズ(size)をページに差し込んだのがつぎのコードです。

src/App.js
function App() {

    // const { getRootProps, getInputProps, isDragActive, open } = useDropzone({
    const { getRootProps, getInputProps, isDragActive, open, acceptedFiles } = useDropzone({

    });

    const files = useMemo(() => 
        acceptedFiles.map(file => (
            <li key={file.path}>
                {file.path} - {file.size} bytes
            </li>
        )
    ), [acceptedFiles]);
    return (
        <div className="container">
            <div {...getRootProps({ style })}>

            </div>
            <aside>
                <h4>Files</h4>
                <ul>{files}</ul>
            </aside>
        </div>
    );
}

書き上がったルートモジュールsrc/App.jsの記述全体をまとめたのが、以下のコード002です。コードの動きは、CodeSandboxに掲げたサンプル003でお確かめください。

サンプル003■ドロップしたファイルの情報がページに示される

2104001_003.png
>> CodeSandboxへ

コード002■領域のスタイルやボタンを加えたファイル選択のインタフェース

src/App.js
import { useCallback, useMemo } from 'react';
import { useDropzone } from 'react-dropzone';

const baseStyle = {
    display: "flex",
    flexDirection: "column",
    width: 200,
    height: 150,
};
const borderNormalStyle = {
    border: "1px dotted #888"
};
const borderDragStyle = {
    border: "1px solid #00f",
    transition: 'border .5s ease-in-out'
};
function App() {
    const onDrop = useCallback((acceptedFiles) => {
        // Do something with the files
        console.log('acceptedFiles:', acceptedFiles);
    }, []);
    const { getRootProps, getInputProps, isDragActive, open, acceptedFiles } = useDropzone({
        onDrop,
        noClick: true
    });
    const style = useMemo(() => (
        { ...baseStyle, ...(isDragActive ? borderDragStyle : borderNormalStyle)}
    ), [isDragActive]);
    const files = useMemo(() => 
        acceptedFiles.map((file) => (
            <li key={file.path}>
                {file.path} - {file.size} bytes
            </li>
        )
    ), [acceptedFiles]);
    return (
        <div className="container">
            <div {...getRootProps({ style })}>
                <input {...getInputProps()} />
                {
                    isDragActive ?
                        <p>Drop the files here ...</p> :
                        <p>Drag 'n' drop some files here</p>
                }
                <button type="button" onClick={open} className="btn btn-primary align-self-center">Select files</button>
            </div>
            <aside className="mt-1">
                <h4 className="mb-0">Files</h4>
                <ul>{files}</ul>
            </aside>
        </div>
    );
}

export default App;
50
27
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
50
27