react-dropzoneとは
react-dropzoneは、ローカルのファイルをドラッグ&ドロップやダイアログで選んで扱うためのライブラリです。ファイルをアップロードするユーザーインタフェースなどに使えます。アップデートは小まめで、本稿執筆時のバージョンはv11.3.2です。最小限のコード例はとても短く、APIにはフックも用いられています(サンプル001)。
サンプル001■最小限のコード例
インストール
コマンドラインツールで、npmやyarnを使ってインストールしてください。
npm install --save react-dropzone
yarn add react-dropzone
本稿の作例は、Create React Appのひな形アプリケーションをもとにつくっています。ひな形のつくり方については「Reactアプリケーションのひな形をつくる」をお読みください。
コード例を試す
使い方(Usage)に掲げられているコード例は、わずか20行足らずです。それでも、コードをコピー&ペーストすれば、最小限のユーザーインタフェースができ上がります。
それに少しだけ、スタイルなどの手を加えたのがつぎのコード001です。コードはたしかに短い。でも、ちょっと何やってるかわからないですね。まずは、冒頭のサンプル001のCodeSandboxコード例で、どういう動きになるのかをご覧ください。
コード001■最小限のコード例
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
プロパティとして渡しても実現できます。
function App() {
return (
// <div {...getRootProps()} style={style}>
<div {...getRootProps({ style })}>
</div>
);
}
ページ内の領域のスタイルは、ドラッグしたとき動的に変えてみましょう。ドラッグしているかどうかは、isDragActive
で調べられました。領域にドラッグしたときのスタイル(borderDragStyle
)には、軽くアニメーション(transition
)も加えています。なお、useMemo
を用いたメモ化については、「Create React App 入門 08: useMemoフックで無駄な再計算を省く」をお読みください。
// 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■ドラッグした領域のスタイルが動的に変わる
ダイアログはボタンで開く
ドラッグ&ドロップはいいとして、ダイアログを開くのはボタンの方がわかりやすそうです。useDropzone()
の引数に{ noClick: true }
を渡すことにより領域クリックでダイアログが開くのは止め、開くための関数open
を取り出します。そして、ボタン(<button>
要素)のonClick
ハンドラからopen
を呼び出すようにしたのがつぎのコードです。
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
)をページに差し込んだのがつぎのコードです。
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■ドロップしたファイルの情報がページに示される
コード002■領域のスタイルやボタンを加えたファイル選択のインタフェース
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;