ReactとFirestoreを使ったチャットアプリ作成メモ

プログラミングスキルを上げるためにReactとFirebaseを使ってチャットアプリを作りました。

メッセージを投稿する機能のみが使えるチャットです。

制作過程で躓いたことなどをメモとして書き記します。

ファイル構成

ファイル構成はこんな感じです。

  Chat-App
	┣━━.firebase
	┣━━functions
	┣━━public
	┣━━━━━src
	┃	┣━━assets
	┃	┃	┣━━image
	┃	┃	┗━━styles
	┃	┃
	┃	┣━━components
	┃	┃	┣━━Chat.js
	┃	┃	┣━━ChatList.js
	┃	┃	┣━━InputChat.js
	┃	┃	┣━━Item.js
	┃	┃	┣━━SubmitChat.js
	┃	┃	┣━━index.js
	┃	┃	┗━━test.js
	┃	┃
	┃	┣━━firebase
	┃	┃	┣━━config.js
	┃	┃	┗━━index.js
	┃	┃
	┃	┣━━App.jsx
	┃	┣━━App.test.js
	┃	┣━━index.js
	┃	┗━━setup.Tests.js
	┃
	┣━━.firebaserc
	┣━━firebase.json
	┣━━firestore.indexes.json
	┣━━firestore.rules
	┣━━package-lock.json
	┣━━package.json
	┗━━yarn.lock

コードの編集を主に行ったのはsrc直下になります。

assetsはCSSなど装飾に関するファイルを格納しています。
本記事では装飾に関する説明は割愛します。

componentsはApp.jsxに使用するパーツを個別に分けて保存しています。

firebaseは文字通りFirebaseに関連するファイルを格納しています。

App.jsxは今回作成したチャットのメインとなるファイルです。
React Hooksを利用して作成しています。

上記がメインのファイルとなります。

Firestoreからのデータ取得に苦戦

Firestore初期設定

まずはFirestoreの初期設定を済ませます。

npm install firebase を実行して、WebGUIでプロジェクトの作成を行います。

さほど難しくはなかったので詳細は省きます。

下記を参考にして初期設定を済ませてください。

https://firebase.google.com/docs/firestore/quickstart?hl=ja

Firebaseのドキュメント

https://qiita.com/shtwangy/items/00229a489b59213700d8

Firebase入門 – 初期環境構築

config.jsとindex.jsを作る

初期設定が完了したらsrc直下にfirebase用のディレクトリを作成し、
その中にconfig.jsとindex.jsを作成します。

ウェブブラウザでFirebaseにログインしてプロジェクトを作成すると
「SDK の設定と構成」という項目があります。

プロジェクト概要の右にある歯車マークをクリックして
マイアプリというところまでスクロールすると確認できます。

「npm」「CDN」「構成」の選択項目があるので「構成」を選択して
下部に表示されるコード const firebaseConfig~ をすべてコピーします。

コピーしたコードはconfig.jsにすべて貼り付けして保存します。

constの前にexportを書いて他のファイルから参照できるようにします。

export const firebaseConfig = {
    apiKey: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
    authDomain: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
    projectId: "XXXXXXXXXXXXXXXXXXXXXX",
    storageBucket: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
    messagingSenderId: "XXXXXXXXXXX",
    appId: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
  };

config.jsはこれでOK

次にfirebaseディレクトリ内にindex.jsを作ってfirebase関連のエントリーポイントとします。

作成したindex.jsはこんな感じ。

import firebase from 'firebase/app';
import 'firebase/auth'
import 'firebase/firestore'
import 'firebase/storage'
import 'firebase/functions'
import { firebaseConfig } from './config';

const firebaseApp = firebase.initializeApp(firebaseConfig);

export const db          = firebaseApp.firestore();
export const auth               = firebaseApp.auth();
export const storage            = firebaseApp.storage();
export const functions          = firebaseApp.functions();
export const FirebaseTimestamp  = firebaseApp.firestore.Timestamp;

export default firebase;

firebase/appからfirebaseをimportします。
これがないとfirebaseを使うことができません。

次にfirebaseで使いそうな機能をimportします。

firebase/auth:firebaseを使った認証機能を簡単に実装できます。
ユーザー登録機能を作る際はこれをインストールします。

firebase/firestore:データベースを使用する際はこれをインストールします。

firebase/storage:ファイルアップロード機能を実装する場合はこれをインストールします。

firebase/functions:バックエンド側の処理を自動化するために使う機能らしい。
(理解不足です・・・。)

先ほど作成したfirebaseConfigをimportします。

import { firebaseConfig } from './config';

firebaseを使用するにあたってはinitializeAppという処理をする必要があります。

constで任意の関数名を定義して、firebase.initializeApp(firebaseConfig)と書きます。
firebaseConfigはimportしたconfigを渡すように指示しています。

const firebaseApp = firebase.initializeApp(firebaseConfig);

次に各機能を使うための任意の関数名を定義して、
initializeAppを実行する関数名の後に使用する機能名を書きます。
そしてその関数を他のファイルから参照できるようにexportしておきます。

例えばdbという関数を定義して、firebaseApp.firestore();を入れています。
データベース関連の機能なのでわかりやすくdbと名前を付けました。

export const db = firebaseApp.firestore();

dbを書き換えると
firebase.initializeApp(firebaseConfig) .firestore(); となります。

configの内容でfirebaseの使用準備(initialize)を行って、firebaseの機能の中のfirestoreを使用する
というのをdbという名前にして他のファイルでも簡単に使用できるようにしました。

その他の機能についても同様ですが、今回のチャットについてはfirestoreのみを使用しています。

App.jsxにてdbとfirebaseをimportすることでfirestoreの機能が使用できるようになります。

import firebase from "firebase";
import { db } from "./firebase";

エントリーポイントを用意する場合、importとexportに誤りがないかをしっかり確認してください。

関数名 not defined というエラーが発生する場合は
このどちらかが(あるいは両方が)正しく書かれていないことが原因の可能性があります。

私は実際にexportされていないもを読み込もうとしてエラーが発生し、
原因究明にかなり時間を費やしてしまいました。

Firebaseのconfigについては、firebase.jsにconfigとエントリーポイントをの役割を持たせた一つのファイルで賄うことの方が一般的みたいです。

折を見て修正しようと思います。

App.jsxでデータ取得用のコード書く

最後にApp.jsx内でデータを取得するためのコードを書けばデータ取得ができます。

db.collection('XXXXXXXXXXX').get();

XXXXXXXXXXXXXにはfirebaseに設定してあるコレクションの名前を入力します。

これが実行されると、XXXXXXXXXXXXXXコレクションの中にあるデータを全件取得することができます。

取得時のデータの並べ替え(orderBy)や取得条件(whereやlimit)といったプロパティがありますので、必要に応じて活用してください。

以下参考

https://firebase.google.cn/docs/firestore/query-data/get-data?hl=ja

Cloud Firestore ドキュメント

https://qiita.com/subaru44k/items/a88e638333b8d5cc29f2

Firebase Cloud Firestoreの使い方

今回のチャットでは、定義した関数をuseEffectの中で実行しています。

取得したデータをuseStateにセットするのに苦戦

前項にて紹介した方法でFirestoreからデータを取得することに成功しました。

 useEffect(() => {
   const getCollection = async() => {
     const docs = await db.collection('user').orderBy('createdAt', 'asc').get();
     let collectionMessages = [];
     docs.forEach((doc) => {
       const data = doc.data();
       collectionMessages.push(data.message);
     });
     setMessageList(collectionMessages, []);
     console.log(MessageList)
   };
   getCollection();
 },[])

これを実行するとMessageListに格納されたmessageが配列としてコンソールに表示されます。

const getCollectionの後に書いてあるasyncは非同期処理を行う際に使用します。

非同期処理を使う理由について、

チャットのメッセージをFirestoreに送信すると、送信した内容が登録されるまで、わずかにディレイが生じます。

データを呼び出そうとしているのに送信したはずのメッセージが未登録だと、エラーが発生してしまうので、送信完了後に登録されるのを待ってデータを呼び出す必要があります。

この処理のことを非同期処理と言います。

次にこの部分でuserという名前のコレクションからデータを全件取得しています。

const docs = await db.collection('user').orderBy('createdAt', 'asc').get();

orderByを使ってcreatedAt(これはメッセージが登録された瞬間のサーバー時間が割り当てられています。)を昇順(asc)に並べ替えています。

これをすることによって、Firestoreに登録されたチャットのメッセージを時系列で並べることができます。

降順に並べる際はdescと書き換えます。

次にこの部分でFirestoreから取得したデータを配列に保存しています。

     let collectionMessages = [];
     docs.forEach((doc) => {
       const data = doc.data();
       collectionMessages.push(data.message);
     });

まず、collectionMessagesという空の配列を用意します。
この配列は、このuseEffectの中だけで使用します。

先ほど定義したdocsという関数の中から一つずつデータを取り出して

     docs.forEach((doc) => {
       const data = doc.data();

collectionMessagesに1つずつ詰め替えます。

       collectionMessages.push(data.message);
     });

最後にuseStateのMessageListにcollectionMessagesの配列を入れることで、
Firestoreからデータを無事に取得し、useStateに格納することができました。

     setMessageList(collectionMessages, []);

最後のこの部分について

   };
   getCollection();
 },[])

usEffectの最後の部分でgetCollectionを実行しています。
実行するコードが書かれていないと、useEffectで処理が行われないためエラーになります。

最後に空の配列を第二引数に定義しています。

useEffectは第二引数の値に変更があった場合に実行されます。

useEffectの第二引数に空の配列を渡すことで、初回のレンダリング時のみ関数を実行することができるようになります。(配列がからの状態は最初だけですからね。)

逆に、第二引数にstateの値などを入れてしまうと、
レンダリング時に値が書き換わるので、useEffectが実行され、
useEffectが実行されることでまたstateが書き換わり、さらにuseEffectが実行されるという
無限ループに陥ります。

useEffectを使う際は第二引数に注意します。

リアルタイムのデータ監視に苦戦

前項で紹介したコードでは、Firestorekara データを取得して表示することができるのですが、
同じアプリケーションを使用している別の方がメッセージを送信した際に、
ページの更新を行わないと表示することができません。

そこでリアルタイムでメッセージの更新を監視して、
更新があった際に自動でメッセージが表示されるようにします。

そのためにはFirestoreに用意されている、onSnapshotというプロパティを使用します。

FirebaseのInitializeを行ったものとして、firebase.firestoreを関数dbに定義した場合、
onSnapshotの使い方はこのようになります。

db.collection('XXXXXXXXXX').onSnapshot(() => {
 ここに実行したい処理を書きます。
}

前項で書いたコードでは、データを取得するための関数を定義して、
関数を実行することで取得とstate(配列)の格納を行っておりました。

onSnapshotでは、データの更新があることで自動的に処理が開始します。

したがって関数を定義する必要がなくなります。

先ほどのコードにonSnapshotを追記してしまうとエラーが発生してしまうため、
上記を踏まえてこのように書き換えました。

  useEffect(() => {
    let data;
    let collectionMessages = [];
    db.collection('user').orderBy('createdAt', 'asc').onSnapshot((snapshot) => {
      collectionMessages =[];
      snapshot.forEach((doc) => {
        data = doc.data();
        collectionMessages.push(data.message);
      });
      setChatList(collectionMessages, []);
      console.log(collectionMessages);
    });
  }, []);

まずuseEffect内で使用する変数を二つ定義します。

    let data;
    let collectionMessages = [];

次にonSnapshotを使ったデータ取得を書きます。

    db.collection('user').orderBy('createdAt', 'asc').onSnapshot((snapshot) => {


    });

データの取得ルールは前項と同じです。

onSnapshotで実行される処理についても基本的には前項と同じです。

      collectionMessages =[];
      snapshot.forEach((doc) => {
        data = doc.data();
        collectionMessages.push(data.message);
      });
      setChatList(collectionMessages, []);
      console.log(collectionMessages);

最後に空の配列を渡して完成です。

これにより、初回のデータ取得とメッセージ追加時のChatListの更新が自動で行われます。

onSnapshotは関数などで定義して、関数を実行するような書き方にしてしまうとエラーが発生します。

onSnapshot自体が処理を実行する役割を持っているので、
書き方には注意が必要です。

まとめ

今回のまとめです。

  • Firebaseはconfigの情報をもとにinitializeAppを行って初めて使えるようになる。
  • Firebaseの各機能を定義したらexportとimportに誤りがないかを確認する。
  • Firestoreからデータを取得する際のプロパティを活用して取得ルールを決めることができる。
  • Firestoreからデータを取得する際は非同期処理を行う。
  • 定義した関数を実行するコードが書かれていないとエラーが発生する。
  • useEffectの第二引数に空の配列を渡さないと無限ループが発生する。
  • リアルタイムなFirestoreの更新とデータ取得にはonSnapshotを使用する。
  • onSnapshotは処理を実行する機能を持っている。
  • 関数内で定義して、関数を呼び出すとエラーになるので注意

以上がチャットアプリを作成して躓いたポイントのまとめです。

現在、チャットアプリにユーザー登録機能、ユーザー認証機能を実装中です。

ユーザー認証機能が完成したら、記事を更新したいと思います。

最後まで読んでいただきありがとうございました。

Follow me!

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です