React & Firebaseで簡単なChatアプリを作ってみた

この記事は Retty Advent Calendar 22日目です。
昨日は、@akiさんの[AWS Cognitoを実践で使うときのハマりどころ](http://qiita.com/aki/items/e35e1bea8c27cfab5e9e)でした。

こんにちは、エンジニアのkawamuraです。

今年も盛り上がりを見せた、React.jsとFirebaseの流行りの波に乗り、簡単なChatアプリを作ってみました。
データベースからホスティングまで全て無料で利用できるFirebaseはとても便利ですね！

作ったアプリは[こちら](https://reactchat-13a37.firebaseapp.com/)です。

# Firebaseの準備

https://console.firebase.google.com/ にアクセスし自身のGoogle accountでログインし、「新規プロジェクト」を作成します。
<img width="431" alt="Screen Shot 2016-12-21 at 21.07.11.png" src="https://qiita-image-store.s3.amazonaws.com/0/132667/a98c3c9d-6d0f-762e-15ff-7c63b42886ae.png">

# Reactプロジェクトの準備

一から環境構築するのが大変なので、`create-react-app`を利用します。

##  導入方法

Node.js version 4以上があれば、以下のコマンドで導入ができます。

```
npm install -g create-react-app 
```

プロジェクトの作成をします。

```
create-react-app react-chat
```
`react-chat`ディレクトリが作成され、必要なファイルとパッケージが全てインストールされます。

## サーバーの開始

サーバー開始用のscriptも用意されてるので、とても簡単です。

```
npm start
```

`localhost:3000`にアクセスすれば、デフォルトのアプリが表示されます。

# Firebase Realtime Databaseとの紐付け

まず、firebaseパッケージをインストールします。

```
npm install firebase --save
```

`firebase/config.js`を作成し、Firebaseの接続情報を記載します。
Firebase consoleから「Overview」 → 「ウェブアプリにFirebaseを追加」から取得できます。

```firebase/config.js
export const firebaseConfig = {
  apiKey: "****************",
  authDomain: "****************",
  databaseURL: "****************",
  storageBucket: "****************",
  messagingSenderId: "****************"
};

```

`firebase/index.js`ではデータベース参照用のインスタンスの作成を行います。
このファイルをimportすれば、どこのファイルからでもデータベースのアクセスを可能にします。

```firebase/index.js
import firebase from 'firebase';
import { firebaseConfig } from './firebase/config.js';

export const firebaseApp = firebase.initializeApp(firebaseConfig);
export const firebaseDb = firebaseApp.database();
```

Firebaseとの紐付けは以上です！簡単ですね！

# Componentの作成

今回のチャットアプリでは二つのComponentを作成します。

- メッセージ表示用のComponent

<img width="524" alt="Screen Shot 2016-12-21 at 21.45.55.png" src="https://qiita-image-store.s3.amazonaws.com/0/132667/ecd074e5-eb75-aea4-747d-5c8dd9610398.png">


```/components/Message.js
export default class Message extends React.Component {
  render() {
    return (
      <div className="Message">
        <img className=""src={this.props.message.profile_image} />
        <div className="">
          <p className="">@{this.props.message.user_name}</p>
          <p className="">{this.props.message.text}</p>
        </div>
      </div>
    );
  }
}

```

- メッセージ入力用のComponent

<img width="505" alt="Screen Shot 2016-12-21 at 21.46.23.png" src="https://qiita-image-store.s3.amazonaws.com/0/132667/89556c9e-aee1-5ca7-3391-49fc0697bf9b.png">


```/components/ChatBox.js
import React from "react";

export default class ChatBox extends React.Component {
  render() {
    return (
      <div className="ChatBox">
        <div className="">
          <input name='user_name' onChange={this.props.onTextChange} className=""  placeholder="名前" />
          <input name='profile_image' onChange={this.props.onTextChange} className="" placeholder="プロフィール画像URL" />
        </div>

        <textarea name='text' className="" onChange={this.props.onTextChange} />
        <button className="" onClick={this.props.onButtonClick}>送信</button>
      </div>
    );
  }
}
```

これらは表示用のComponent(Presentational Component)なので、`onClick`や`onChange`などのイベント処理は`props`として受けとり、Containerで実装します。

# Containerの作成

Containerはデフォルトで用意されているApp.jsを利用します。

まず、簡単な骨組を作り、stateの初期化をする。
stateで以下の状態を監視する。

- text → 入力された、メッセージのテキスト
- user_name → 入力された、ユーザー名
- profile_image → 入力された、プロフィール画像のURL
- messages → 保存されている、すべてのメッセージ


```App.js
import React, { Component } from 'react';
import './App.css';
import { firebaseDb } from './firebase.js'
import Message from './components/message.js'
import ChatBox from './components/chatBox.js'

const messagesRef = firebaseDb.ref('messages')

class App extends Component {
  constructor(props) {
    super(props);
    this.onTextChange = this.onTextChange.bind(this)
    this.onButtonClick = this.onButtonClick.bind(this)
    this.state = {
      text : "",
      user_name: "",
      profile_image: "",
      messages : []
    }
  }

  render() {
    return (
      <div className="App">
        <div className="App-header">
          <h2>Chat</h2>
        </div>
        <div className="MessageList">
          {this.state.messages.map((m, i) => {
            return <Message key={i} message={m} />
          })}
        </div>
        <ChatBox onTextChange={this.onTextChange} onButtonClick={this.onButtonClick} />
      </div>
    );
  }
}

export default App;
```

次に、文字が入力されているたびstateの更新を行うためのイベント処理の実装します。
`ChatBox`の`onTextChange`にpropsとして受け渡してます。
これでユーザー名、メッセージのテキスト、画像URLが変更されるたびにstateが更新されます。

```App.js
//...
  onTextChange(e) {
    if(e.target.name == 'user_name') {
      this.setState({
        "user_name": e.target.value,
      });
    } else if (e.target.name == 'profile_image') {
      this.setState({
        "profile_image": e.target.value,
      });
    } else if (e.target.name == 'text') {
      this.setState({
        "text": e.target.value,
      });
    }
  }
//..
```


次に、送信ボタン押下時のイベント処理の実装します。
firebaseのpushメソッドを利用し`messages`内にに追加します。
pushメソッドは新しい要素を追加するたびに一意のIDを生成します。これによって生成されるIDはタイムスタンプに基づいているため、リストのアイテムは自動的に時系列に並び替えられます。

これも同様に、`ChatBox`の`onButtonClick`にpropsとして受け渡してます。

```App.js
//...
  onButtonClick() {
    // 簡単なバリデーション
    if(this.state.user_name == "") {
      alert('user_name empty')
      return
    } else if(this.state.text == "") {
      alert('text empty')
      return
    }
    messagesRef.push({
      "user_name" : this.state.user_name,
      "profile_image" : this.state.profile_image,
      "text" : this.state.text,
    })
  }

//..
```


最後に、データベースの変更をキャッチするリスナーの実装です。
データベースの`messages`内に新たな子が追加されるたびにstateの`messages`も更新され、Viewも更新される仕組みなってます。

```App.js
//..
  componentWillMount() {
    messagesRef.on('child_added', (snapshot) => {
      const m = snapshot.val()
      let msgs = this.state.messages

      msgs.push({
        'text' : m.text,
        'user_name' : m.user_name,
        'profile_image' : m.profile_image,
      })
      
      this.setState({
        messages : msgs
      });
    })
  }
//..
```

# デプロイ
デプロイ用のコマンドラインツールをインストールし、初期設定とログインをします。

```
$ npm install -g firebase-tools
$ firebase init
$ firebase login
```

firebaseのデプロイ先はデフォルトがpublicなので、以下のようにfirebase.jsonを変更します。

```firebase.json
{
  "database": {
    "rules": "database.rules.json"
  },
  "hosting": {
    "public": "build"
  }
}
```
また、databaseのアクセスルールも変更します。

```database.rules.json
{
  "rules": {
    ".read": "true",
    ".write": "true"
  }
}
```

buildをし、firebaseにdeployします。

```
$ npm run build
$ firebase deploy
```

# 最後に

Firebaseがここまでサービスを無料で提供しているのはとても素晴らしいことだと思います。
簡単なウェブアプリをサクッとつくってみてはいかがでしょうか。

# 参考
- https://facebook.github.io/react
- https://firebase.google.com/docs/database/web/start?hl=ja
- http://qiita.com/gonta616/items/278a7e81a8b624d9621e
- http://codepen.io/bradleyboy/pen/zxLaoP?editors=1010