Firebase&React&Reduxで多機能チャットを実装しよう【リアルタイムチャット編】
今回から本格的にReactとReduxを触っていきます。
一気に難易度が上がるので覚悟してください!笑
ガイド
- 環境準備編
- リアルタイムチャット~React編~ ←イマココ
- リアルタイムチャット~Redux編~
- ログイン機能
- チャットルーム選択機能
- チャットルーム設定機能
- デモページ
- GitHub
ディレクトリ構成
前回までのおさらい。
ディレクトリ構成は現在以下の通りです。
要らないファイルを削除して、ディレクトリを追加作成します。
react-chat
│- build/
│- public/
│ │- favicon.ico
│ │- index.html
│ │- logo192.png <-- REM
│ │- logo512.png <-- REM
│ │- manifest.json
│ └ robots.txt
│- src/
│ │- firebase/
│ │ │- config.js
│ │ └ index.js
│ └ index.js
│- node_modules/
│- .firebaserc
│- database.rules.json
│- firebase.json
│- package.json
│- package-lock.json
└ storage.rules.json
react-chat
│- build/
│- public/
│ │- favicon.ico
│ │- index.html
│ │- manifest.json
│ └ robots.txt
│- src/
│ │- components/ <-- ADD
│ │- containers/ <-- ADD
│ │- firebase/
│ │ │- config.js
│ │ └ index.js
│ │- templates/ <-- ADD
│ │- index.js
│ └ style.css <-- ADD
│- node_modules/
│- .firebaserc
│- database.rules.json
│- firebase.json
│- package.json
│- package-lock.json
└ storage.rules.json
React基本ファイルの準備
create-react-appで作成した開発環境の基本を簡単に説明しておきます。
-
public
:雛形となるhtmlファイルを格納するディレクトリ -
src
:コンパイル前のソースコードを格納するディレクトリ -
npm run build
:src
配下のファイルをコンパイルするコマンド -
build
:コンパイルされたファイルを格納するディレクトリ
雛形となるHTMLファイル
雛形となるHTMLファイルには以下を記載します。
- サイトのタイトルやメタ情報
- Firebaseに必要なスクリプト
- Reactコンポーネントをrenderするルートdiv
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Firebase React Chat!</title>
<!-- update the version number as needed -->
<script defer src="/__/firebase/7.5.1/firebase-app.js"></script>
<!-- include only the Firebase features as you need -->
<script defer src="/__/firebase/7.5.1/firebase-auth.js"></script>
<script defer src="/__/firebase/7.5.1/firebase-database.js"></script>
<script defer src="/__/firebase/7.5.1/firebase-messaging.js"></script>
<script defer src="/__/firebase/7.5.1/firebase-storage.js"></script>
<!-- initialize the SDK after all desired features are loaded -->
<script defer src="/__/firebase/init.js"></script>
</head>
<body>
<div id="root"></div>
</body>
</html>
Reactコンポーネントをindex.jsファイル
npm run build
でコンパイルする際に参照されるjsファイルです。
細かい説明は省きますが、後ほど登場するファイルたちの情報をまとめておきます。
import React from 'react';
import * as ReactDOM from 'react-dom';
import {Provider} from 'react-redux';
import {RootContainer} from './containers';
import {configureStore} from './modules';
import './style.css'
ReactDOM.render(
<Provider store={configureStore()}>
<RootContainer />
</Provider>,
document.getElementById('root')
);
ReactDOM.render(element, container)
で、Reactコンポーネント(element)を雛形HTMLのDOM(container)にレンダーして表示させています。
先ほど作成したHTMLファイル内の、idがrootのdiv要素にレンダーさせているということですね。
それでは、レンダーされるコンポーネントを作っていきましょう。
Reactコンポーネントの作成
ReactをReduxと共に用いる場合、コンポーネントは2つに分けるのが一般的です。
- Presentational Components --> 見た目(View)を担当する。「コンポーネント」と呼ばれる
- Container Components --> ロジック(振る舞い)に関わる。「コンテナー」と呼ばれる。
より詳細な定義はコチラの記事を参考にしてください。
上記を踏まえて、ディレクトリ構成で追加したディレクトリに格納するファイルの役割は以下の通りです。
-
components
: 最小構成要素(パーツ)となるPresentational Components、子コンポーネントです。 -
templates
:components
内のパーツを組み合わせたPresentational Components、親コンポーネントです。 -
containers
:templates
のPresentational Componentsと"状態"を紐づけるための中継役、Container Componentsです。
ログインページを例にすると
components
は「ボタン」や「テキストボックス」
templates
は「ボタン」や「テキストボックス」を組み合わせた「ログインフォーム」
containers
はログイン処理後の見た目(View)がどのように変化するのか定義
少し難しいですよね。
「コンポーネントは見た目(View)のみを持ち、状態(State)と分離させる」というReduxの設計思想を実現するための構成だと思ってください。
それでは、実際にリアルタイムチャットを実装するためのファイルを書いていきます。
componentsファイルの作成
チャットを実装するためには以下のパーツが必要だと考えました。
- メッセージ表示エリア
- メッセージ入力エリア
- メッセージ送信ボタン
この記事では2と3について解説します。
当記事ではMaterial-UIを使っていますが、詳しい解説を割愛します。
デザインのカスタマイズには癖があるけど、それっぽいUIが簡単に作れて便利だゾ。
また、デモページを作った都合上、独自CSSを使っています。
CSSはGithubのソースコードから確認してください。(src/style.css
)に記述しています。
import React from 'react';
import TextField from '@material-ui/core/TextField';
const TextInput = (props) => {
return (
<form className="p-chat__textarea c-grid-center" noValidate autoComplete="off">
<TextField
id="standard-text"
className="c-grid-full"
margin="normal"
label="メッセージを入力..."
multiline
rowsMax="4"
onChange={e => props.onChange(e.target.value)}
value={props.value}
/>
</form>
);
};
export default TextInput;
import React from 'react';
import Button from '@material-ui/core/Button';
import SendIcon from '@material-ui/icons/Send';
const SendButton = (props) => {
return (
<Button
variant="contained"
color="primary"
className="p-chat__button-send"
onClick={() => props.onClick(props.value, props.roomId, props.fromId, props.toId, props.userIds)}>
<SendIcon />
</Button>
);
}
export default SendButton;
それぞれのポイントを解説します。
両コンポーネントは、Stateless Functional Componentsという形式で宣言されています。
初期のReactでは、Classを用いてコンポーネントを宣言していました。
近年のReactでは、Stateless Functional Components、つまり"関数"として宣言することが推奨されています。
Stateless Functional Componentsには以下のメリットがあります。
- constructorを使わなくて良い
- thisが必要ない
- 状態を持たない(stateless)コンポーネントにできる
つまり、コードがシンプルになってハッピーってことですね☆
また、ES6のアロー関数を使ってさらにシンプルに書いています。
そして重要なのが、関数の引数に渡しているprops
です。
この部分const TextInput = (props) => {
です。
この部分const SendButton = (props) => {
ですよ。
props
は親コンポーネントから渡された引数を一挙に受け取ります。
親コンポーネントからvalue
として渡した引数を、子コンポーネントでprops.value
のように参照することができます。
props.onClick
やprops.onChange
は、状態を変更するためのActionsを呼び出します。
Actionsについては[次の記事]で解説します。
templatesファイルの作成
templates
ディレクトリ配下にChat.js
を作成します。
Chat.js
は親コンポーネントとして、先ほど作成したコンポーネントをまとめます。
ファイルが長いので部分ごとに解説します。
import
ファイル冒頭のimport文です。
import React, {Component} from 'react';
import {Chat, Common} from '../components';
import {database} from '../firebase/index'
components/index.js
でexport
した"Chat"と"Common"のコンポーネントをimport
します。
constructorとrender
class ChatTemplate extends Component {
constructor(props) {
super(props);
}
/* 中略 */
render() {
return (
<div className="p-chat">
<Common.NavBar
value={this.props.messages}
actions={this.props.actions.messages}
back={this.props.actions.messages.backToRooms}
configure={this.props.actions.messages.configure}
signOut={this.props.actions.messages.signOut}
/>
<div className="p-chat__area" id="scroll-area">
{this.props.messages.msgs.map((m, i) => (
<Chat.AlignItemsList key={i} msgs={m} />
))}
</div>
<div className="c-grid__row">
<Chat.TextInput
onChange={this.props.actions.messages.change}
value={this.props.messages.value}
/>
<Chat.SendButton
onClick={this.props.actions.messages.submit}
value={this.props.messages.value}
roomId={this.props.messages.roomId}
fromId={this.props.messages.userId}
toId={this.props.messages.partnerId}
userIds={this.props.messages.userIds}
/>
</div>
</div>
);
}
}
export default ChatTemplate
親コンポーネントではcomponentDidMount()
などのライフサイクルを使いたいので、Stateless Functional ComponentsではなくClass Componentsで宣言します。
constructorの宣言をします。
this.props
でreduxのStoreに保存されているGlobal Stateを参照できます。
(ごめんなさい、Storeについても[次の記事]で解説します...!)
constructor(props) {
super(props);
}
続いて、render
部分です。
return()
のなかにDOMやコンポーネントを記述します。
render() {
return (
/*
レンダーするDOMを記述する。
作成した子コンポーネントもこの中で宣言して使う。
*/
)
}
"Chat"としてimportした子コンポーネントを呼び出す方法は以下の通りです。
例...TextInputコンポーネントを呼び出す。
return(
<Chat.TextInput />
)
さらに、引数を渡してみましょう。
return(
<Chat.TextInput
onChange={this.props.actions.messages.change}
value={this.props.messages.value}
/>
)
少し分かりづらいかもしれませんが
-
this.props.actions.messages.change
というActionsをonChange
としてTextInputコンポーネントに渡している -
this.props.messages.value
というGlobal Stateをvalue
としてTextInputコンポーネントに渡している
これによって、TextInputコンポーネントではprops.onChange
やprops.value
として渡された値を参照できる。
ライフサイクルメソッド
ReactのClass Componentsではライフサイクルメソッドが使えます。
ライフサイクルメソッドが分かりやすく図解されている記事はコチラ
代表的なメソッドは以下。
-
componentWillMount()
--> 現在は非推奨なので使わない。 -
render()
--> Viewを描画する。 -
componentDidMount()
--> API連携など、通信が必要な処理はここで。 -
componentDidUpdate()
--> stateが変更されて再render()が走った後に -
componentWillUnmount()
-->componentDidMount()
で確保したリソースを解放する
src/templates/Chat.js
では各メソッドで以下の処理を実行している。
-
componentDidMount()
--> Firebase DBにメッセージのデータが追加されたらViewを再描画するようにリスナーを仕掛ける。 -
componentDidUpdate()
--> メッセージ一覧の最下部にスクロールする。 -
componentWillUnmount()
--> Firebase DBへのリスナーを解除する。
ソースコード載せると長〜いので、Githubを確認してください🙏
リスナーの設定はfirebase.database().ref().on()
で。
逆にリスナーの解除はfirebase.database().ref().off()
です。
リスナー設定/解除の詳細はFirebaseクライアントSDKのドキュメントを参照。
データの取得|Firebase Realtime Database
containersファイルの作成
最後にContainer Componentsを作成します。
import ChatTemplate from '../../templates/Chat';
import {bindActionCreators, compose} from 'redux';
import {connect} from 'react-redux';
import {actions} from '../../modules/chat/index';
const mapStateToProps = state => {
return {
messages: state.messages,
};
};
const mapDispatchToProps = dispatch => {
return {
actions: {
messages: bindActionCreators(actions.messages, dispatch),
},
};
};
export default compose(
connect(
mapStateToProps,
mapDispatchToProps
)
)(ChatTemplate);
すでに説明した通り、Container Componentsの役割は中継役です。
ReactとReduxを繋げるreact-redux
というライブラリのconnect
メソッドを使います。
connect
メソッドは、以下2つをReactコンポーネントで参照できるようにしているイメージです。
-
mapStateToProps
: Reduxで管理しているstate(状態) -
mapDispatchToProps
: state(状態)を変更するためのReduxのActions
上記のContainer Componentsの書き方は汎用的に使えるはずです。
まとめ
Reactだけならまだしも、Reduxが絡むと途端にややこしくなります。
特にContainer Componentsでconnect
するあたりは、最初何をやっているのか全く理解できませんでした。
分からなくてもコピペしておきましょう。
この記事で少しでも理解を深めていただければ幸いです。
Reactは公式のドキュメントが充実しているので、(自戒も込めて)よく読んだ方がいいですね。
[次の記事]でははいよいよReduxのActions, Reducers, Storeなどを解説していきます。