#はじめに
現在何を制作しているのかという話からすると、
とある情報をインターネット上でクローリングして取得し、その情報をブラウザ上で確認できるダッシュボードのようなWebアプリを制作しています。
ざっくりとした設計はこんな感じです。
- Pythonでクローリングし、取得した情報をFirestoreに登録。
- フロントエンドでFirestoreに登録された情報をフェッチし、一覧にして表示する。
- 画面上から期間とカテゴリを選択し、データをcsvでダウンロードできるようにする。
今回はこのデータをcsvでダウンロードできるようにする
部分のお話です。
react-csv
というライブラリを使用したのですが、どこで詰まり、どのようにして解決したのかをまとめます。
まず react-csv とは
React上で簡単にcsvエクスポート機能を実装できるライブラリです。
似たようなライブラリもいくつかありましたが、ダウンロード数を見る限りこれが一番メジャーなのではないかと思い選択しました。
使い方はドキュメントに記載されていますが、このようにとてもシンプルです。
import { CSVLink, CSVDownload } from "react-csv";
// csvファイルの1行目にあたる部分を headers で指定できる
headers = [
{ label: "First Name", key: "firstname" },
{ label: "Last Name", key: "lastname" },
{ label: "Email", key: "email" }
];
// csvファイルの2行目以降、要するにデータを指定できます。
// sampleでは以下のようにデータがベタ書きされていますが、
// 今回のアプリではFirestoreからフェッチしたデータをここで指定するイメージです。
data = [
{ firstname: "Ahmed", lastname: "Tomi", email: "ah@smthing.co.com" },
{ firstname: "Raed", lastname: "Labes", email: "rl@smthing.co.com" },
{ firstname: "Yezzi", lastname: "Min l3b", email: "ymin@cocococo.com" }
];
// 以下の2つからどちらか適切なほうを選択します。
<CSVLink data={data} headers={headers}>Download me</CSVLink>;
// または
<CSVDownload data={csvData} headers={headers} target="_blank" />;
CSVLink と CSVDownload の違いについて
-
CSVLink
はマウントされた<CSVLink>
をクリックすることでcsvがダウンロードされます。
ですのでタグの中にクリックを促すメッセージを書くことになるでしょう。
<CSVLink>"ここにクリックを促すメッセージを書く"</CSVLink>
-
CSVDownload
は<CSVDownload />
がマウントされたタイミングで自動的にcsvがダウンロードされます。
どちらを使うべきか?
要件によって選択を求められますが、
今回は以下の点から<CSVDownload />
が適切と判断しました。
- ユーザーがカテゴリと期間を決定するタイミングで1クリックが発生する
- csvとして吐き出せるのは当然フェッチが完了してから
- となるとCSVLinkの場合はフェッチ後に再度1クリックを促すことになる
なのでフェッチできたかどうかのステートを用意し、
true
であれば<CSVDownload />
をマウントするようにしました。
//省略
//フェッチの完了を判断するステート(完了後に true とする)
const [fetchDone, setFetchDone] = useState(false);
//省略
<button
className={styles.fetchButton}
onClick={() => {
fetch(category, startDate, endDate);
}}
>
エクスポート
</button>
{fetchDone && <CSVDownload data={data} headers={headers} />}
カテゴリと期間を選択し、「エクスポート」というボタンをクリックするとフェッチを開始。
完了とともにCSVDownload
がマウントされ、csvファイルがダウンロードされるという流れです。
これは問題なく機能しました。
しかし、その後面倒な問題が発生しました。
CSVDownloadの問題点: csvのファイル名を指定できない
「ダウンロードするcsvのファイル名を指定したい」
という新しい要件が出たタイミングで問題に直面しました。
ちなみにreact-csvにはダウンロードするcsvのファイル名を指定するオプションが用意されています。
<CSVLink filename={"my-file.csv"}>Download me</CSVLink>;
このようにfilename
で指定すればよいだけなのですが、
なぜかこのオプションはCSVLinkのみに提供されていて、CSVDownloadでは使えないのです。
(指定がない場合はハッシュ値のようなランダムなファイル名になります)
確認する限り2017年からリクエストがあげられているようですが、まだ機能として追加されていないようです。(2021年3月現在)
・react-csv/issues/47
かと言って単純にCSVLinkに置き換えてユーザーに2回クリックを要求することは避けたい。
どうすべきか、、というところで一つ思いつきました。
「そうだ、フェッチ完了後にプログラム側でCSVLinkをクリックさせよう!」
CSVLinkは適当にどこか見えないところに置いておいて、
フェッチ完了後にこれをクリックさせれば、CSVDownloadとほぼ変わらない挙動を実現できるのではないか・・・
useRefを活用してcsvLinkをプログラム上からクリックする
リアルDOMを操作することになるのでuseRef
を使います。
※ 命名のセンスは見逃してください..
const fetchDoneRef = useRef();
作成したfetchDoneRef
をCSVLink
に埋め込みます。
<button
className={styles.fetchButton}
onClick={() => {
fetch(category, startDate, endDate);
}}
>
エクスポート
</button>
{fetchDone && <CSVLink
data={data}
headers={headers}
filename={"hoge.csv"} //ファイル名を指定
ref={fetchDoneRef} // refを指定
/>
}
そしてフェッチの処理が書いてある関数内で、フェッチ完了後にクリックする処理を追加します。
fetchDone && fetchDoneRef.current.link.click();
これで期間とカテゴリを選択し「エクスポート」ボタンをクリックすると...
うん、問題なく1回のクリックでダウンロードされました。動きはCSVDownloadとまったく同じです。
いい感じです!
そして実際は選択した期間やカテゴリによってファイル名を指定したいので、
filename={`${category}-${start}-${end}.csv`}
こんな感じでfilenameを指定すると..
うん、やりたかったことができています!
少し力技っぽい感が否めないですが、できているので良しとします。
まとめ
CSVDownload
っぽい動きをさせたいけどファイル名も指定したい。
そんな時はuseRef
を活用しCSVLink
をクリックすることで解決できますというお話でした。
csvエクスポートっていう要件自体がニッチだと思いますが、参考になれば幸いです。