今回はNext.jsとFirestoreを使ってCSVのインポート機能とエクスポート機能の作り方を解説していきます。
また、ただCSVをインポートして保存するだけではなく、インポートする際にCSVのデータにバリデーションをかけるやり方についても、この記事で説明していきますね。
デザインに関してはChakra UIを使って整えます。
開発環境
- macOS Ventura 13.2.1
- Next.js 13.4.9
- Chakra UI 2.7.1
- Firebase 10.0.0
必要なライブラリをインストールする
ターミナルに以下のコマンドを入力して、必要なライブラリをNext.jsのプロジェクトにインストールしてください。
・Firebase
yarn add firebase @types/firebase
・react-dropzone
yarn add react-dropzone
・react-papaparse
yarn add react-papaparse
・react-csv
yarn add react-csv
yarn add @types/react-csv
・Chakra UI
yarn add @chakra-ui/react @emotion/react @emotion/styled framer-motion
今回使うCSVデータ
今回は以下のようなCSVデータを使います。
科目ID | 科目名 |
---|---|
456789 | 数学 |
123456 | 英語 |
789123 | 世界史 |
654321 | 日本史 |
789456 | 地理 |
321654 | 化学 |
234567 | 物理 |
876543 | 生物 |
908172 | 古典 |
908172 | 現代文 |
このCSVの科目IDがドキュメントIDがなり、科目名がドキュメントのnameフィールドとして保存されるように処理を書いていきます。
ページコンポーネントを作成する
まずは、ページコンポーネントから作成していきます。pagesディレクトリにcsv_test.tsxというファイルを作成してください。
csv_test.tsxの中身は一旦、以下のようにしておいてください。
const Csv_test = () => {
return (
<div>
CSV
</div>
);
}
export default Csv_test;
これからCSVをインポートするコンポーネントとエクスポートするコンポーネントを作成して、その2つのコンポーネントをcsv_test.tsxのページに表示させるようにしていきます。
CSVをインポートするコンポーネントを作成する
CSVインポートの機能から先に作成していきます。
プロジェクトにcomponentsディレクトリを作成して、その中にCsvImport.tsxというファイルを作成します。
CsvImport.tsxの中身は以下のようにしてください。
import { useCallback } from 'react';
import React, { useState } from "react";
import { useDropzone } from 'react-dropzone';
import { readString } from 'react-papaparse';
import {
Box,
Button,
} from '@chakra-ui/react';
const CsvImport = () => {
// CSVをドロップしたときに呼び出される処理
const onDrop = useCallback((acceptedFiles: any) => {
acceptedFiles.forEach((file: any) => {
const reader = new FileReader();
reader.onabort = () => console.log('file reading was aborted');
reader.onerror = () => console.log('file reading has failed');
reader.onload = () => {
const binaryStr = reader.result;
// CSVのデータをコンソールに表示する
console.log(binaryStr);
}
reader.readAsText(file);
});
}, []);
const {getRootProps, getInputProps, isDragActive} = useDropzone({onDrop});
return (
<div className="App">
<div {...getRootProps()}>
<input {...getInputProps()} />
{ isDragActive ? <p>Drop the files here ...</p>
: <Button>ファイルを選択</Button>
}
</div>
</div>
);
}
export default CsvImport;
ここまでできたら、CsvImport.tsxに作成したCSVをインポートするコンポーネントをページコンポーネントで読み込みます。
csv_test.tsxを以下のように編集してください。
import {
Box,
} from '@chakra-ui/react';
import CsvImport from '@/components/CsvImport';
const Csv_test = () => {
return (
<Box>
<Box p={3}>
<Box mb={2}>
<p>CSVインポート</p>
</Box>
<CsvImport/>
</Box>
</Box>
);
}
export default Csv_test;
実際に画面を開いてCSVを選択すると、コンソールにCSVのデータが出力されていると思います。
CSVのデータをFirestoreに保存する
では、CsvImport.tsxにCSVとして読み込んだデータをFirestoreに保存するための処理を追加していきます。
Firestoreにsubjectというコレクションを作成して、先ほどのCSVの科目IDをドキュメントID、科目名をドキュメントのnameフィールドとして保存されるようにします。
まず、CsvImport.tsxに以下のような関数を追記してください。
// CSVのデータをFirestoreに保存する関数
const HandleFileRead = (binaryStr: any) => {
readString(binaryStr, {
worker: true,
complete: async (results: any) => {
// FirestoreにCSVデータを保存する処理 (results.dataは配列になったCSVデータ)
for (let i = 1; i < results.data.length; i++) {
// CSVデータの1列目をドキュメントIDとして指定
const docRef = doc(db, "subject", results.data[i][0]);
// CSVデータの2列目をドキュメントのnameフィールドに指定してFirestoreに保存
await setDoc(docRef, {
name: results.data[i][1],
});
}
}
});
}
HandleFileRead関数はCSVのデータをFirestoreに保存するための関数です。Firestoreのsubjectコレクションにデータを保存しています。
Firestoreに保存するsetDocメソッドを使うときにawaitを使うため、繰り返しはmap関数ではなくfor文を使っています。
また、CSVデータの最初の1行目はヘッダーになっているので、for文の繰り返しは0ではなく1からスタートするようにしていますね。
このHandleFileRead関数を先ほどのonDrop関数の中で呼び出します。
// CSVをドロップしたときに呼び出される処理
const onDrop = useCallback((acceptedFiles: any) => {
acceptedFiles.forEach((file: any) => {
const reader = new FileReader();
reader.onabort = () => console.log('file reading was aborted');
reader.onerror = () => console.log('file reading has failed');
reader.onload = () => {
const binaryStr = reader.result;
// CSVのデータをFirestoreに保存する処理
HandleFileRead(binaryStr);
}
reader.readAsText(file);
});
}, []);
ここまでできたら、実際に画面を開き、CSVをインポートしてみてください。CSVのデータがFirestoreに保存されているはずです。
CSVデータのバリデーション
続いては、先ほどのCSVデータをFirestoreに保存する処理に、CSVデータのバリデーションをつけていきます。
まずは、CsvImport.tsxのコンポーネントにエラーメッセージを表示するためのタグを追加していきます。
return (
<div className="App">
<div {...getRootProps()}>
<input {...getInputProps()} />
{ isDragActive ? <p>Drop the files here ...</p>
: <Button>ファイルを選択</Button>
}
</div>
{/* CSVバリデーションのエラーメッセージ */}
<Box id="csv_import_error_message" color={'red'}></Box>
</div>
);
もしインポートしたCSVがバリデーションで弾かれた場合は、このBoxタグでエラーメッセージを表示させます。
次は、バリデーションの処理を書いていきます。
今回は科目IDと科目名に必須のバリデーションをつけていきますね。インポートしたCSVの科目IDと科目名に1つでも値が入っていないセルがあった場合にバリデーションで弾かれるようにしていきます。
バリデーションの処理も先ほど作成したFirestoreにデータを保存するための処理と同様に、HandleFileRead関数のcompleteの中に書いていきます。
HandleFileRead関数を以下のように変更してください。
// CSVのデータをFirestoreに保存する関数
const HandleFileRead = (binaryStr: any) => {
readString(binaryStr, {
worker: true,
complete: async (results: any) => {
// エラーメッセージの初期化
(document.getElementById('csv_import_error_message') as HTMLElement).innerHTML = '';
// バリデーションの処理
for (let i=0; i<results.data.length; i++) {
// 科目IDもしくは科目名が入力されていないときのバリデーション
if (!results.data[i][0] || !results.data[i][1]) {
(document.getElementById('csv_import_error_message') as HTMLElement).innerHTML = '科目IDもしくは科目名が入力されていないセルがあります。';
return false;
}
}
// FirestoreにCSVデータを保存する処理 (results.dataは配列になったCSVデータ)
for (let i = 1; i < results.data.length; i++) {
// CSVデータの1列目をドキュメントIDとして指定
const docRef = doc(db, "subject", results.data[i][0]);
// CSVデータの2列目をドキュメントのnameフィールドに指定してFirestoreに保存
await setDoc(docRef, {
name: results.data[i][1],
});
}
}
});
}
このバリデーションは科目IDか科目名のどちらか一方が空文字になっているときに、エラーメッセージのBoxタグのテキストを「科目IDもしくは科目名が入力されていないセルがあります。」に変更するような処理になっています。
また、一度インポートして失敗したときにエラーメッセージが出て、再度インポートして成功したにもかかわらずエラーメッセージが表示され続けるということがないように、CSVのインポートが行われるたびにエラーメッセージは初期化されるようにしておきます。
これでバリデーションの実装は完了です。
実際に、CSVのデータを
科目ID | 科目名 |
---|---|
456789 | 数学 |
123456 | 英語 |
789123 | 世界史 |
654321 | 日本史 |
789456 | 地理 |
321654 | 化学 |
234567 | 物理 |
876543 | |
908172 | 古典 |
908172 | 現代文 |
という感じに変更します。あえて科目IDが876543のところだけ科目名を空欄にしています。このCSVをインポートしてみるとバリデーションでインポートできないようになっていることが確認できると思います。
これでCSVのインポート機能は完成です。
CSVをエクスポートするコンポーネントを作成する
続いては、Firestoreに保存されているデータをCSVとして出力する機能ですね。
まずは、CSV出力を行うためのコンポーネントを作成していきます。componentsディレクトリにCsvExport.tsxというファイルを作成してください。
このファイルの中身は以下のように書きます。
import React, { useEffect, useState } from 'react';
import { CSVLink, CSVDownload } from "react-csv";
import {
Box,
Button,
} from '@chakra-ui/react';
const CsvExport = () => {
// CSVLinkをクライアントサイドでのみレンダリングするためのuseState
const [isClient, setIsClient] = useState(false);
// CSVとしてエクスポートするデータのヘッダー
const headers = [
{ label: "科目ID", key: "subjectId" },
{ label: "科目名", key: "subjectName" }
];
// CSVとしてエクスポートするデータ
const csvExportData = [
{ subjectId: "456789", subjectName: "数学" },
{ subjectId: "123456", subjectName: "英語" },
{ subjectId: "789123", subjectName: "世界史" },
{ subjectId: "654321", subjectName: "日本史" },
{ subjectId: "789456", subjectName: "地理" },
{ subjectId: "321654", subjectName: "化学" },
{ subjectId: "234567", subjectName: "物理" },
{ subjectId: "876543", subjectName: "生物" },
{ subjectId: "908172", subjectName: "現代文" },
];
useEffect(() => {
// isClientをtrueにする
setIsClient(true);
}, []);
return (
<Box>
{
isClient &&
<CSVLink data={csvExportData} headers={headers} filename={"subject.csv"}>
<Button>
CSVエクスポート
</Button>
</CSVLink>
}
</Box>
);
}
export default CsvExport;
ユーザーがCSVLinkのタグをクリックすると、CSVがダウンロードされるようになっています。ダウンロードされるCSVのヘッダーはheadersという定数になっていて、データの部分はcsvExportDataという定数になっています。
現時点ではCSVとして出力するデータはハードコーディングしていますが、このあとFirestoreからデータを取得して、そのデータを出力するようにしていきます。
また、CSVLinkはサーバーサイドとクライアントサイドでのレンダリング結果が一致しないといったエラーが出る場合があります。
その場合は、useEffectを使ってCSVLinkのコンポーネントがクライアントサイドでのみレンダリングされるようにすれば解決できます。今回の実装でもそのようにしています。
FirestoreのデータをCSVとして出力する
では、CsvExport.tsxにFirestoreからデータを取得する処理を追加していきます。
まずはFirestoreから取得したデータを格納するためのuseStateを定義します。
// csvエクスポート用のデータを格納するuseState
const [ csvExportData, setCsvExportData ] = useState<any>([]);
※ハードコーディングしたcsvExportDataの配列は消しておきます。
次は、Firestoreからデータを取得する関数を追記します。
// CSVとしてエクスポートするデータを作成する処理
const getCsvExportData = async () => {
const subjects: any = [];
const subjectQuerySnapshot = await getDocs(collection(db, "subject"));
subjectQuerySnapshot.docs.map((doc)=>{
subjects.push({
subjectId: doc.id,
subjectName: doc.data().name
});
});
setCsvExportData(subjects);
}
Firestoreからsubjectコレクションのドキュメントのスナップショットを取得し、ドキュメントIDとnameフィールドを持つオブジェクトとして、配列に1つ1つ入れていき、その配列をuseStateに格納するというのが、この関数でやっていることですね。
また、この関数はuseEffectを使って実行します。
useEffect(() => {
// CSVとしてエクスポートするためのデータを取得する処理を実行
getCsvExportData();
// isClientをtrueにする
setIsClient(true);
}, []);
CSVをエクスポートするコンポーネントがレンダリングされるときに、Firestoreからデータを取得するという感じです。
ここまでできたら、実際に画面を開いてCSVがエクスポートできるかどうかを確認してみてください。
まとめ
react-dropzoneやreact-csvなどのライブラリを使うことでNext.jsでCSVのインポートやエクスポート機能をつくることができる。
今回の実装のgithubのリポジトリはこちらです。