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

[![react-dropzone](https://raw.githubusercontent.com/react-dropzone/react-dropzone/master/logo/logo.png)](https://react-dropzone.js.org/)

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

#### サンプル001■最小限のコード例
[![2104001_001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/37716691-22c1-58a6-49e6-d7bbf3a3ed75.png)
\>> CodeSandboxへ](https://codesandbox.io/s/react-react-dropzone-basic-example-ue6jt)


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

```terminal
npm install --save react-dropzone
```

```terminal
yarn add react-dropzone
```

本稿の作例は、[Create React App](https://create-react-app.dev/)のひな形アプリケーションをもとにつくっています。ひな形のつくり方については「[Reactアプリケーションのひな形をつくる](https://www.fumiononaka.com/Business/html5/FN2101001.html#sec01)」をお読みください。


# コード例を試す
使い方([Usage](https://github.com/react-dropzone/react-dropzone#usage))に掲げられているコード例は、わずか20行足らずです。それでも、コードをコピー&ペーストすれば、最小限のユーザーインタフェースができ上がります。

それに少しだけ、スタイルなどの手を加えたのがつぎのコード001です。コードはたしかに短い。でも、ちょっと何やってるかわからないですね。まずは、冒頭のサンプル001の[CodeSandboxコード例](https://codesandbox.io/s/react-react-dropzone-basic-example-ue6jt)で、どういう動きになるのかをご覧ください。

#### コード001■最小限のコード例
```react: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`](https://developer.mozilla.org/docs/Web/API/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`プロパティとして渡しても実現できます。

```react:src/App.js
function App() {

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

		</div>
	);
}
```

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

```react: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/3917939a-2626-aa7f-847f-3a028cc0093f.png)
\>> CodeSandboxへ](https://codesandbox.io/s/react-react-dropzone-setting-style-dynamically-zxp2m?file=/src/App.js)


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

```react: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`)をページに差し込んだのがつぎのコードです。

```react: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128949/e9dbbae9-5f4d-4ff5-d4ef-d3b5d0d541f1.png)
\>> CodeSandboxへ](https://codesandbox.io/s/react-react-dropzone-selection-button-and-file-info-2buji?file=/src/App.js)

#### コード002■領域のスタイルやボタンを加えたファイル選択のインタフェース
```react: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;
```