React + Firestore【入門から実装まで】

本稿ではFirestoreの概要、NoSQLデータベースの構造、Firestoreの初期設定、Reactの初期設定、書き込み、react-firebase-hooks(useCollection、useDocument)を使った読み込み方法(実装方法)まで丁寧に解説しています。書き込みはweb Version 8での記法と、2021年8月に正式リリースされたより新しいweb Version 9での記法を両方解説しています。

Firestoreとは

FirestoreとはFirebase(Google Cloud Platformとほぼ同義)内で利用できるのNoSQLのデータベースで、最大の特徴はReact等フロントエンドから直接DBを操作できる点です。バックエンドを記述する必要がなく、フロントエンド側でFirebaseライブラリーを使ってDBへの書き込み・読み込みが簡単にできます(もちろんバックエンド側からも接続できます)。下記のイメージ図はいわゆるMERNスタック(上段)と、今回ご紹介するReact + Firestore構成(下段)の比較です。後者はシンプルな構造です。

なおフロントエンド(クライアント)からのリクエストの処理(アクセス権限)は、MERNスタックではバックエンドで管理しますが、Firestoreでは独自のセキュリティールールで設定します。今回はセキュリティールールを定めないテスト環境でご紹介します。




Firebase内のFirestoreをデータベースとするなら、ホスティングもFirebaseを使用するのが無難です(他も使用可能)。Firebaseは以前のこちらの記事でもご紹介したように、AWSのような複雑な設定なくして、簡単にWebサービスを立ち上げることができるインフラです。

大規模なWebサービスでは、これまでフロントエンドエンジニア、バックエンドエンジニア、それに主にAWSを念頭にインフラエンジニアの3職種(3技能)の連携が不可欠でした。一人で性格の異なる3技能をこなすのは容易ではなかったのですが、Firebase(インフラ)とFirestore(データベース)を使用することで、フロントエンドエンジニアが簡単にWebサービスを立ち上げられるようになりました。他方で両者はAWSと双璧を成すGCP(Google Cloud Platform)の上で動いていることから、簡単であるといっても大規模なWebサービス(重いアクセス)にも対応できます。

またFirestoreでは、Firebaseに以前からあるリアルタイムデータベースのように全てのデータを読み込んでクライアントと同期させるのではなく、都度必要な部分だけを読み込む方法を採用することで、処理がより高速化されています。

Firestoreのコレクション – ドキュメント構造

続いてFirestoreで採用されているNoSQL構造(うち、ドキュメント指向)について、ご紹介します。NoSQLデータベースは、下記Wilipediaの定義にある通り、リレーショナルデータベース以外のDBという消極的概念で、異なる様々なDB構造の総称です。

NoSQL(一般に “Not only SQL” と解釈される)とは、関係データベース管理システム (RDBMS) 以外のデータベース管理システムを指すおおまかな分類語である。関係データベースを杓子定規に適用してきた長い歴史を打破し、それ以外の構造のデータベースの利用・発展を促進させようとする運動の標語としての意味合いを持つ。

Wikipedia

いずれのNoSQLデータベースにおいても共通する点は、RDBMS(エクセル)のようにテーブル(シート)、カラム(列)、レコード(行)を持たない点で、Firestoreで採用されているドキュメント指向でも同様です。

Firestoreは、下記のようなCollection(コレクション) – Document(ドキュメント)構造をしています。コレクションは、リレーショナルデータベース(RDB)のテーブルに相当し、ドキュメントはRDBではテーブル内では一つのまとまったデータ、すなわち行に対応します。コレクションとドキュメント構造は、下記のように起点のRoot(ルート)から始まり、コレクションに繋がります。ルートから始まるコレクション(ルートコレクション)は下記の図では1つですが、複数あってもかまいません。

コレクションは、その内側に複数のドキュメントを保持します。言い換えると、ドキュメントの集合がコレクションです。一つ一つのドキュメントは自動的に付与される固有のキー(指定も可)と、Javascriptのオブジェクト(json)と同様のkey : valueのペアで構成されるデータを持ちます。Firestoreでは、このドキュメント内の個々の内容をField(フィールド)と呼んでいます。ドキュメントをRDBに例えると行に相当するとしましたが、フィールドのkeyはRDBの列に相当し、フィールドのvalueはRDBでは行と列が交わる部分の個々のデータに相当します。なお、コレクションには固有のキーが自動的に付与されず、コレクションの名称が固有のキーの役割を果たします。

上記イメージ図の右上にある通り、ドキュメントはその内容としてフィールドに加えて、サブコレクションを持つこともできます。チャットアプリを例に挙げると、(1)チャットのルームの集まりがコレクション、(2)各チャットルームのデータを保持するのがドキュメント(フィールドとしてルーム名を保持)、(3)各チャットルーム内のチャットの集合は(2)から派生するサブコレクション、(4)各チャットメッセージは同サブスクリプション内のドキュメントで、ユーザー情報、チャット本文、添付ファイル、投稿日時などのフィールドを持つといった具合です。

下記はFirestoreで実際に上記チャットルームのデータベースを組み立てた例です。(1)に対応するのが最も左側にあるroomsです。中央には二つのキーがあり、それぞれが(2)ドキュメント(チャットルーム)に対応します。このドキュメントはフィールドとして、右側下段のチャットルーム名{name: “New Channel”}を保持する他、右側上段に(3)サブコレクションとして”messages”を持ちます。コレクション及びドキュメントは、Firestoreの画面上から作成できる他、後述するReact内のコードからも作成可能です。



サブコレクション”messages”はチャットの集合ですので、中を開くと下記のようにそれぞれのチャットがドキュメントとして格納されています。下記は上記とそっくりに見えますが、最も左側のコレクションがroomsではなく、サブコレクションの”messages”になっています。Firestoreの画面上では1階層のコレクション-ドキュメントしか表示されないので、階層をサブコレクションに掘り下げると、上位のコレクション階層は画面上からは消えます。

messagesサブコレクションですが、中央のドキュメントのキーを見ると、2件のドキュメント(チャット)が投稿されていることがわかります。また、最も右側を見ると、うち1件のチャットのドキュメントに係るフィールドとして、メッセージの内容、投稿の時間、ユーザー名、ユーザーイメージのURLの4件の情報が格納されていることがわかります。




後ほど詳しく解説いたしますが、ReactからFirestoreのデータにアクセス(書き込み又は読み込み)するときは、次のようなコレクションとドキュメントが連なったコードを書きます。web Version 8の記法ですが構造はweb Version 9でも同じです。



db.collection(…).doc(…).collection(…).doc(…)

Reactにおけるfirestoreの使い方


Firebase上の設定

Firestoreを使用する前に、Firebaseでの初期設定(アプリ登録等)が必要になります。私のこちらの記事「FirebaseでWebアプリをデプロイする方法」の1章「Firebaseのサイト上での初期設定」と、8章「Firebaseプロジェクト内でのアプリの登録方法」でご紹介した手順に従って設定をします。また、同記事の2章の冒頭にある次のコードも実行します。2章の以降の内容と、7章まではアプリが完成した後のホスティングの方法のご紹介ですので、Firestoreの設定をしている開発段階ではスキップします。

npm install firebase
npm install -g firebase-tools


firebase.js

次のコードは8章の最後でご紹介したfirebase.jsからfirestoreに最低限必要な部分だけを抜粋したものです(firebaseConfigの内容は省略)。

1,2行目で必要なライブラリーをインポートして、4行目をConfigを設定した後、同Configを引数とするinitializeAppオブジェクトを設定、appに同オブジェクトを格納しています。InitializeAppがフロントエンドとfirebaseを接続する役目を果たしています。

const dbとある部分で、appを引数としたfirestoreと接続し、接続をdbと定義、最終行で同dbをエクスポートしています。dbの中にはConfigの内容も含まれてフロントエンドとfirestoreの接続が維持されるので、他のシートでこのdbオブジェクトをインポートして使用するだけでfirestoreとの接続が可能です。

firebase.js

import { initializeApp } from "firebase/app";
import { getFirestore } from "firebase/firestore";

const firebaseConfig = {
// configを転記
};

const app = initializeApp(firebaseConfig);
const db = getFirestore(app);

export default db;

Firestoreの立ち上げ

firebase.jsの作成を終えたら、Firebaseのサイト内でFirestoreデータベースの設定をします。Firebaseのホームページに戻り、console画面に入ります。前述Firebase上の設定で作成したプロジェクトを選択します。


プロジェクト内に入ったら、左サイドバーからFirestore Databaseを選択します。中央のCloud Firestoreを選択しても同じです。



続いて画面中央のCreate databaseをクリックします。



production modeかtest modeを選ぶポップアップが出ますので、いったんtest modeで始めます。本番運用するときに、別途詳細なセキュリティールールを定めてproduction modeに移行します。



前の画面で右下のNextを選ぶとサーバの場所を選べますので、US centralなどMulti-regionのどこかを選択します。最後に右下のenableを押すと下記のような画面になります。Firestoreの立ち上げは30秒ほどで終わります。



テスト用のセキュリティールールの設定


セキュリティールールでは、データベースのアクセス権限を定めます。ドキュメント毎に、読み込み、書き込み、消去の細かな設定ができます。test modeの初期設定では、誰でもデータベースにアクセスできるようになっていますが、期間が1ヶ月間に制限されています。いったんこの1ヶ月制限を切ります。Cloud Firestoreの画面でRulesを選択し、下記画面の灰色の部分を消去します。最後のセミコロンは残します。



消去の後、Publishを選択します。前述したように、このテスト環境の設定では誰でもデータベースを操作できるので、本番運用時はセキュリティールールの設定を要します。



Firestoreへの書き込み – Web version 8



次に具体的にCollection(コレクション) – Document(ドキュメント)構造のFirestoreデータベースを作成します。ここでは、ドキュメント指向データベースの説明で例として用いたチャットのデータベースを作りたいと思います。

コレクションの追加は、Firestoreの画面からでもGUI操作でできるのですが、先ほどdbオブジェクトを作りましたので、フロントエンド側からの操作方法をご紹介します。

Chat.jsというコンポーネント内に設置したボタンをクリックすると、addChatという関数を呼び出され、その関数によってFirestore内に起点のrootから繋がる最上位のコレクション”rooms”内にドキュメントを作成したいと思います。各ドキュメントの名称はpromptでユーザーが決められるようにします。下記は、Chat.jsの関数部分です。ボタンを設置するJSXの部分(returnの内側)の表記は割愛しています。



Chat.js

import React from “react”;
import db from “./firebase”;

function Chat() {
const addChat = () => {
const chatName = prompt(“Please enter the chat room name”);

if (chatName) {
db.collection(“rooms”).add({
name: chatName,
});
}
};

return (
);
}

export default Chat;

2行目で先ほどfirebase.jsで作成・エクスポートしたdb(フロントエンドからFirestoreへの接続オブジェクト)をインポートしています。

関数コンポーネントChat内の1行目でAddChat関数を定義し、関数内ではまず、promptでユーザーからのインプットをchatNameに格納しています。これは後にドキュメントの名称(チャットルームの名前)になります。

続いて、db.collection(“rooms”)の部分で最上位のroomsコレクションを設置し、.addメソッドでこのコレクションにドキュメントを加えています。.addメソッドにはnameをkeyに、ユーザーからのインプットをvalueとしたオブジェクトを引数として渡します。このオブジェクトは冒頭のfirestoreの構造で紹介したフィールドに相当し、オブジェクト構造の任意のデータを格納できます。以上の操作でroomsコレクションの中に、ユーザーがインプットした値(ここではチャットの部屋の名称)を保持するドキュメントが作成されます。

実際に、フロントエンドから上記の操作をして、Firestoreを見ると次のようになります。promptでは”TestChatRoom”と入力しました。

冒頭の説明を繰り返すと、左側のroomsがコレクション、中央の暗号のようなものがコレクション内にあるドキュメントの固有のキー、右側にあるのがドキュメントの内容にあたるフィールドです。ドキュメントの中には.addで加えてオブジェクト{name : “TestChatRoom”}が格納されています。


サブコレクションを加える方法

ドキュメントの下に更にサブコレクションを加える場合、次のようなコードになります。idの部分にはドキュメントの固有キーを入れます。ドキュメントの固有キーは、Reduxなどを使いユーザーからのインプットに応じて格納したキー(doc.id)を使用するのが通常だと思いますが、firestoreの画面にある具体的なキー、例えば”ge3QDAXvi4yTZfATwRo6″を.doc()に直接引数として渡すこともできます。サブコレクションの名称はここでは”room”としました。.add以降は上記の1階層の場合と同様に、key:valueペアからなるドキュメントの内容(フィールド)を記述します。

db.collection(“rooms”).doc(id).collection(“room”).add{
}


Firestoreデータベースの読み込み方法

Firestoreデータベースからの読み込みには、以前こちらの記事でもご紹介したreact-firebase-hooskを使います。まずはインストールします。

npm install react-firebase-hooks

raect-firebase-hooksには、Firestoreを読み込むhooksが全部で8つ(コレクションを読むhooksが4つ、ドキュメントを読むhooksが4つ)用意されていますが、始めにコレクションを読み込むuseCollectionを、コンソール画面に表示するだけの簡単な例でご紹介します。コンポーネントの名称は仮にReadDb.jsとし、JSXの部分は省略しています。

useCollection

関数コンポーネントの冒頭ではuseCollectionと、firebase.jsでエクスポートしたdbをインポートします。

関数の内側の1行目でuseCollectionを呼び出しています。useCollecitonはコレクションを読むhooks(関数)ですので、引数にはいずれかのコレクションを指定します。ここでは最上位に指定したroomsコレクションを指定しています。

戻り値は(1)コレクションの内容の他、(2)ロード中か否か(boolean)を示すloadと、(3)エラーが返ってきた場合の内容が格納されるerrorの3つがあります。名称は(2)がloading、(3)はerrorとするのが通例ですが(1)はなんでも構いません。公式ドキュメントではsnapshotやvalueという名称を使用していますが、ここではわかりやすく(1)をcollectionsとして受けています。


ReadDb.js

import React from “react”;
import { useCollection } from “react-firebase-hooks/firestore”;
import db from “./firebase”;

function ReadDb() {
const [collections, loading, error] = useCollection(db.collection(“rooms”));
collections?.docs.map((doc) => {
console.log(doc.data().name);
console.log(doc.id);
});

return (
);
}

export default ReadDb;

先ほどご紹介したように、コレクションは複数のドキュメントの集合ですので、具体的に内容を見るために、collections?.docsのコードでコレクションの内部のドキュメントの集合を呼び出しています。今回の例ではドキュメントは1つですが、通常、コレクション内には複数のドキュメントがあるのでmap関数を用いてループ処理します。

data()メソッドでドキュメントの中のフィールド(実際のデータ)にアクセスできます。data()メソッドを適用しない場合は、ドキュメントのメタデータも含んだオブジェクトが返されます。今回のフィールドの内容は{name : “TestChatRoom”}の1対ですので、.nameとすると”TestChatRoom”が呼び出され、上記コードではコンソール画面に表示されます。なお、前述サブコレクションはフィールドの一部ではないので、data()メソッドで得られる戻り値にも含まれておりません。

続く行のdoc.idはドキュメントに固有のidを呼び出しています。Firestoreの画面上とコードを結びつけると次のようなイメージになります。

useDocument

最後にuseDocumentをご紹介します。こちらは名前の通り、複数のドキュメントの集合のコレクションではなく、特定のドキュメントの内容を取得します。ここでは、roomsコレクションの中の特定のドキュメント内に、roomサブコレクションがあり、さらにその中のドキュメントがあるとします(前述「サブコレクションを加える方法」)。

useDocumentのインポートの方法は、useCollectionと同様です。関数の戻り値が3つある点も一緒です。引数の内容は異なり、useCollectionではコレクションを渡しましたが、useDocumentはドキュメントを渡します。.doc(id)のidの部分には前述の通り、ドキュメントに固有のidを渡します。

useDocumentは内容が特定の一つのドキュメントですので、useCollectionのようにmap関数を使わずに、ダイレクトに.data()メソッドを呼び出してフィールドを取得・表示できます。


ReadDb.js

import React from “react”;
import { useDocument } from “react-firebase-hooks/firestore”;
import db from “./firebase”;

function ReadDb() {
const [docDetails, loading, error] = useDocument(
db.collection(“rooms”)
.doc(id)
.collection(“room”)
.doc(id)
);
console.log(docDetails?.data());
};

return (
);
}

export default ReadDb;

Firestoreへの書き込み – Web version 9

version 9での書き込み方法につき、Firestoreの公式ドキュメントでは、addDocの内側にcollectionを入れ子とする書き方と、setDocの内側にdocを入れ子とする書き方の2通りがあります。使い方が少し異なりますので順番に説明いたします。

addDocとcollectionの組み合わせ

公式ドキュメントで紹介されている例は次の通りです。addDocの1番目の引数で、ドキュメントを設定するコレクションへのレファレンスを指定します。addDocの2番目の引数では、具体的に書き込みたいドキュメントの内容、すなわちフィールドを渡します。try catchブロックとawaitは後ほど説明します。


Firestore Web version 9 addDoc collection

ややわかりにくいaddDocを2行に分けると次のようになります(tryブロクの内側のみ表示)。1行目ではコレクションを作成し、そのコレクションへのレファレンス(参照)を得ます。collectionの1番目の引数dbは、Firebaseのコンフィギュレーションファイルで作成しエクスポートしたdbです。collectionの2番目の引数”users”はコレクションの名称の事例です。コレクションでは名称が固有のidを兼ねるには前述の通りです。2行目以降のaddDocでは、1行目で作成したコレクションの中に、ドキュメント及びその具体的内容であるフィールドを作成します。addDocの1番目の引数は、コレクションへのレファレンス(参照)、2番目の引数はドキュメントの具体的内容たるフィールド(オブジェクト)です。


const collectionRef = await collection(db, "users");
await addDoc(collectionRef, {
first:"Ada",
last:"Lovelace",
born:1815
});

addDocとcollectionを組み合わせる方法の特徴は次の通りです。

  1. コレクションが存在しない場合は新たに作成
  2. コレクションが存在する場合は既存のコレクションを参照
  3. ドキュメントの追加時にidを自動生成

3点目の特徴は便利ですが、反対にidを自分で指定できないことが欠点です。ログインユーザーのautheticationのuidをFirestoreのidと一致させたいとき、別のDBの外部キーを充てたい場合などにはaddDocとcollectionは使えず、次のsetDocとdocを使います。

setDocとdocの組み合わせ

公式ドキュメントで紹介されている例は次の通りです。setDocの1番目の引数で、Documentを設定するドキュメントへのレファレンスを指定します。setDocの2番目の引数では、具体的に書き込みたいドキュメントの内容(フィールド)を渡します(2番目の引数はaddDocと同じ)。addDocとの違いは下線部です。addDocではコレクションを指定し、その内側に新たなドキュメントを作成したのに対して、setDocではコレクションの内側のドキュメントまで指定して、その中に具体的なフィールドを作成します。ドキュメントを具体的に指定するので、idはFirestoreによる自動生成ではなく指定する必要があります。また、ドキュメントが存在しない場合は指定したidでドキュメントを新たに作成してくれます。


Firestore Web version 9 setDoc doc

こちらも公式ドキュメントがわかりにくいので、setDocの中身を2段に分解してみます(tryブロクの内側のみ表示)。1行目ではドキュメントを指定、又は、新たに作成し、そのドキュメントへのレファレンス(参照)を得ます。docの1番目の引数dbは、Firebaseのコンフィギュレーションファイルで作成しエクスポートしたdbです(collectionと同じ)。docの2番目の引数”cities”はコレクションの名称(id)の事例です。2行目以降のsetDocでは、1行目で指定又は作成したドキュメントの中に、その具体的内容であるフィールドを作成します。setDocの1番目の引数は、ドキュメントへのレファレンス(参照)、2番目の引数はドキュメント(フィールド)です。


const documentRef = await doc(db, "cities", 'LA' );
await setDoc(documentRef, {
name:"Los Angeles",
state:"CA",
country"USA"
});

setDocとdocを組み合わせる方法の特徴は次の通りです。

  1. コレクションは新規作成できない(既存のものを指定する)
  2. ドキュメントも原則新規作成しない(idにより既存のものを指定する)
  3. 指定したidのドキュメントが不存在の場合は、同idでドキュメントを新規作成

1点目につき、コレクションはfirestoreの画面で予め作ることで対応します。2,3点目につき、idを指定する必要がある(指定できる)点がsetDocとdocを組み合わせる方法の特徴です。よって、authenticationのuidやその他データベースからの外部キーをidとして使用することができます。

onSubmitを通じたFirestoreへの書き込み

ユーザーにデータベースへの書き込みを認める場合は、フォームとonSubmitを組み合わせることが多いと思います。ログインしたユーザーが、自身のアカウントと紐付く内容に限ってデータベースに書き込める設計はよく使われるもやや複雑になるので、本章では誰でもデータベースに書き込めるという前提で紹介します。いずれ記事に書きたいと思いますが、サポートが必要な方ははこちらからご相談をください。

下記はReactのコンポーネントの例です。formタグにonSubmit属性を付け、form内のボタンを押すと、onSubmit関数が呼び出され、ローカルステートに格納されたformの情報(ここではuseState)がFirestoreデータベースに登録されるという構造です。formの情報をローカルステートに格納する方法はこちらの記事「FormでのuseRefの使い方」で詳しく解説しているのでここでは割愛します。本事例ではuseRefではなくuseState使い、Firestoreへの書き込みには、addDocとcollectionを用いました。


StoreToDb.js

import { useState } from "react";
import { collection, addDoc } from "firebase/firestore";
import db from "./firebase";


function StoreToDb() {
const [name, setName] = useState("");
const [email, setEmail] = useState("");

const onSubmit = async (e) => {
e.preventDefault();
try {
await addDoc(collection(db, "users"), {
name,
user
});
} catch (error) {
console.log(error);
}
};

return (
<div>
<form onSubmit={onSubmit}>
<label htmlFor="name">Input here</label>
<input
type="text"
id="name"
onChange={(e) => setName(e.target.value)}
/>
<input
type="text"
id="email"
onChange={(e) => setEmail(e.target.value)}
/>
<button type="submit">
Submit
</button>
</form>
</div>
);
}

export default StoreToDb;

順に解説いたします。始めの3行で必要なライブラリーをインポートします。ローカルステートのuseStateを、Firestoreへの書き込みにはcollecitonとaddDocをインポートします。3行目のdbはライブラリーではなく、Firebaseコンフィギュレーションファイルで作成して、エクスポートしたdbです。よって”./firebase”の部分はファイルを格納したパスによって変わります。

関数コンポーネントの内部では、始めにuseStateを2本設定しす。Firestoreへデータをポストする前に、ローカルステートにデータをいったん格納する目的です。初期値は空の文字列です。続いて、onSubmit関数を定義します。Firestoreへの書き込みと応答のプロセス全体を非同期処理としたいので、onSubmit関数をasync関数とます。e.preventDefalut()はリロードを防ぐ目的です。続くtryのブロックでawaitを付けて”users”コレクションを参照してその内部にドキュメントとその具体的内容たるフォームを追加しています。catchではエラーを処理します。

フィールドはオブジェクトです。JavaScriptのオブジェクトは原則keyとvalueのペアが必要ですが、本事例のようにkeyの名称とvalueの変数名が一致しているときは省略した書き方が可能です。省略しないと次のようになります。formとonChangeの部分は先ほどご紹介した私の別の記事をご参照ください。


{
name:name,
email:email
}

今日も最後まで読んで頂きありがとうございました。