React NativeとCloud Firestoreで(5分では無理だけど)チャットアプリを作る

## はじめに

### この記事について

[Firebase Advent Calendar 2017](https://qiita.com/advent-calendar/2017/firebase) 20日目の記事です。

昨日は @daikiojm さんの [Firebase Realtime DatabaseとAngularでフォロー/フォロワー機能の構築をしてみた](https://qiita.com/daikiojm/items/abb5aedbf3dc765cb183) でした。
Realtime DatabaseやCloud Firestoreのスキーマ設計は、RDBとは違った考え方をしなければ行けないにも関わらず日本語の情報が十分でないように感じますので、少しでも知見が溜まっていのは有り難いですね！


本記事は @imaimiami さんの [new FirebaseとReact NativeでiOS, Android向けチャットを５分で作る](https://qiita.com/imaimiami/items/2bf9e47f68d0e791586b) でRealtime Databaseを使っていたものを、Cloud Firestoreでやってみようという位置づけになります。
(以下、省略してFirestoreと記載します。)

### 5分では無理な理由

「5分で」の記事では `firebase` モジュールを使っていたのですが、本記事では `react-native-firebase` を導入するためです。

### 環境
``` package.json
{
  ...
  "dependencies": {
    "react": "16.0.0",
    "react-native": "0.51.0",
    "react-native-firebase": "^3.2.0",
    "react-native-gifted-chat": "^0.3.0"
  },
}
```
で確認済みです。

## `firebase` vs `react-native-firebase`

Advent Calendar 2日目の @k2wanko さんの記事 [React Native Firebaseについて #firebase #fjug](https://qiita.com/koki_cheese/items/e9f165563b3dadb0da03) でも触れられていますが、React NativeからFirebaseを使う際は、

* [firebaseモジュール](https://www.npmjs.com/package/firebase) (WebSDK)
* [react-native-firebaseモジュール](https://www.npmjs.com/package/react-native-firebase)

のどちらかのnpmモジュールを導入することになります。

ざっくりと、 `firebase` はJavaScript上のみで動くのに対し、 `react-native-firebase` はiOSおよびAndroidネイティブのSDKをReact Native用にラップしたものです。

READMEに[対応表](https://github.com/invertase/react-native-firebase#supported-firebase-features)がありますが、 `firebase` からのFirestoreの使用はpartial supportとなっています。(2017年12月時点)
Firestoreがベータ版で登場した10月ころは、 `react-native-firebase` のみが対応となっていました。このようにバージョンアップ対応が早いのと、パフォーマンス面でも優れているらしいので、React NativeからFirebaseを使うときは、基本的に `react-native-firebase` を導入するといいでしょう。

## Firebaseプロジェクトの設定

### Firebaseプロジェクト作成
[Firebaseコンソール](https://console.firebase.google.com/)からプロジェクトを追加します。
プロジェクト名などは適当に入力します。

### iOSおよびAndroidアプリの登録
ダッシュボードから「iOSアプリにFirebaseを追加」もしくは「AndroidアプリにFirebaseを追加」をクリックし、アプリを登録します。
このとき、iOSの場合はバンドルID、Androidの場合はパッケージ名を入力します。
ステップ2以降はReact Nativeプロジェクト作成後に行うので、今はアプリの登録だけで大丈夫です。

### Firestoreの有効化
「Database」メニューから、「Firestoreベータ版を試してみる」をクリックします。
今回は簡単に試したいので、セキュリティルールは「テストモードで開始」を選択します。
![firestore-chat_–_Database_–_Firebase_console.png](https://qiita-image-store.s3.amazonaws.com/0/27764/00164143-905e-b098-17de-b2ec40f7e403.png)

## React Nativeの初期設定

### プロジェクトのinit
`react-native-firebase` はネイティブのSDKを使うため、Expo(create-react-native-app)は使えません。

```
$ react-native init [APP_NAME]
$ cd [APP_NAME]
```

### react-native-firebaseの依存追加

以下のドキュメントに沿ってreact-native-firebaseモジュールを追加し、iOSおよびAndroidプロジェクトにFirebase SDKを追加していきます。
手順が多いので割愛します:bow:

* [React Native Firebase - Simple Firebase integration for React Native](https://rnfirebase.io/docs/v3.2.*/getting-started)
* [Firebase を iOS プロジェクトに追加する  |  Firebase](https://firebase.google.com/docs/ios/setup) (要CocoaPods)
* [Android プロジェクトに Firebase を追加する  |  Firebase](https://firebase.google.com/docs/android/setup)

このとき、Firebaseへの依存関係として、iOSのPodfileには

```
pod 'Firebase/Core'
pod 'Firebase/Firestore'
```

を、Androidのbuild.gradleには

```
compile "com.google.firebase:firebase-core:11.4.2"
compile "com.google.firebase:firebase-firestore:11.4.2"
```

を追加しておきます。
また、プロジェクト作成時のiOSのバンドルIDやAndroidのパッケージ名は適当なものになっているので、Firebaseプロジェクトで指定したものと一致するように変更が必要です。

### react-native-gifted-chatの依存追加
[react-native-gifted-chat](https://github.com/FaridSafi/react-native-gifted-chat)を使うことでチャットUIがすぐに利用できます！

```
$ yarn add react-native-gifted-chat
```

## React Nativeコードの実装



### 基本のチャット画面作成
いよいよJSXを触っていきます。
[react-native-gifted-chatのExample](https://github.com/FaridSafi/react-native-gifted-chat#example)を参考に、App.jsに `render` と `onSend` を実装していきます。[^1]

``` App.js
import React, { Component } from 'react';
import { GiftedChat } from 'react-native-gifted-chat';

export default class App extends Component {
  state = {
    messages: [],
  };

  /**
   * Sendボタンがタップされたときのイベント
   */
  onSend = (messages = []) => {
    // massagesをstateに渡す
    this.setState((previousState) => ({
      messages: GiftedChat.append(previousState.messages, messages),
    }));
  }

  render() {
    return (
      <GiftedChat
        messages={this.state.messages}
        onSend={this.onSend}
        user={{
          _id: 1,
          name: 'John Doe'
        }}
      />
    );
  }
}
```
これだけのコードでチャット画面が表示されます。

### Firestoreとの連携を追加
今の状態ではアプリを閉じるとチャットが消えてしまいますので、messagesをFirestoreに保存するように変更していきます。

``` App.js
import React, { Component } from 'react';
import { GiftedChat } from 'react-native-gifted-chat';
import firebase from 'react-native-firebase';

export default class App extends Component {
  state = {
    messages: [],
  };

  componentDidMount() {
    // Firestoreの「messages」コレクションを参照
    this.ref = firebase.firestore().collection('messages');

    // refの更新時イベントにonCollectionUpdate登録
    this.unsubscribe = this.ref.onSnapshot(this.onCollectionUpdate);
  }

  componentWillunmount() {
    // onCollectionUpdateの登録解除
    this.unsubscribe();
  }

  /**
   * Sendボタンがタップされたときのイベント
   */
  onSend = (messages = []) => {
    // Firestoreのコレクションに追加
    messages.forEach((message) => {
      this.ref.add(message);
    });

    // onCollectionUpdateが呼ばれるので、ここではstateには渡さない
    //this.setState((previousState) => ({
    //  messages: GiftedChat.append(previousState.messages, messages),
    //}));
  }

  /**
   * Firestoreのコレクションが更新されたときのイベント
   */
  onCollectionUpdate = (querySnapshot) => {
    // docsのdataをmessagesとして取得
    const messages = querySnapshot.docs.map((doc) => {
      return doc.data();
    });

    // messagesをstateに渡す
    this.setState({ messages });
  }

  render() {
    return (
      <GiftedChat
        messages={this.state.messages}
        onSend={this.onSend}
        user={{
          _id: 1,
          name: 'John Doe'
        }}
      />
    );
  }
}
```

### Firebaseコンソールで確認する
ここまでうまくできていれば、アプリ側で入力したメッセージがFirestoreに保存され、他の端末からも見れるようになっているはずです。

「すべてのドキュメントを削除」したときの**おおおお！**という感じを味わっていただきたいです。[^2]
![firestore-15-32-18-3.gif](https://qiita-image-store.s3.amazonaws.com/0/27764/2cee2547-9641-dbad-a98b-cd084b72bd14.gif)

## この先の実装方針
このままではドキュメントのIDがランダムで設定されるため、メッセージが正しい順番で表示されません。(上のスクリーンショットでも発生しています)
IDの頭にタイムスタンプをつけて時系列順になるようにするとコンソール上でも順番が一致するので、そのようにするのがよさそうです。

また、実際のチャットアプリでは、ユーザーの情報があったり複数のチャットルームが存在したりするので、それに応じてスキーマやセキュリティルールも変わってくると思います。

## さいごに
先日、 [Firebase Japan User Group](https://firebase.asia/) の第1回meetupが開催されました！
Firebaseを使っていたり興味があるけど話せる相手が少ないという方は、ぜひSlackやmeetupにご参加ください。

![logo.png](https://qiita-image-store.s3.amazonaws.com/0/27764/21ed27a8-2e22-9814-c0c5-00f6349c184d.png)

[^1]: [Handling Events - React](https://reactjs.org/docs/handling-events.html) を参考に、public class field syntaxを使っています
[^2]: この画面では conversations -> test_chat -> messages というスキーマになっていて、掲載しているApp.jsとは若干表示が異なります