Firebase Advent Calendar 2016 16日目が空いていたので、お邪魔します。
@SatoTakumiさん < > @Sakai Fumiyaさん
FRP + Reactシリーズです。なにそれ?って人は、こちらの記事を先にご覧ください。
ところでFirebase便利ですよね。それではReactを使う上でFirebase使用する際には、どうしたらいいのでしょうか? 今回はFirebaseのいくつかある機能のうちRealtime Databaseに着目していきたいと思います。
デモ
イメージを掴みたいという方は先に、デモをどうぞ。(これもFirebaseの機能の一つ、Hostingです。デプロイがラクチンですよね。)
先にリポジトリは、こちら
Firebase Realtime Database
まず、FirebaseのRealtime Databaseを使うには、Referenceを取得する必要があります。チャットルームなどを分けたい場合は、Referenceを分けるなどすると良いでしょう。
const chatRef = Firebase.database().ref("chat");
データベースの読み込みと、書き込みは、以下の2つを使います。
chatRef.on("child_added", (snapShot) => {
// コールバックにデータ一つずつ流れてくる。
// 最初は、データベースの値が全て流れてくる。
snapShot.val();
...
});
chatRef.push(message);
Firebase と FRP React
さすが、Firebaseとっても単純なAPIです。しかしながらReactで使う場合は、Reactのコンポーネントのライフサイクル等を気にする必要が出てきて、Stateもどのように書き換えていくか複雑になってしまいます。本記事では、Pure Reactとの比較までは行わないので、ご自分で実装して大変さを味わってみてください。
React+FRPでのアプローチは、ごくごく単純な工夫でFirebaseとReactの組み合わせが可能です。まずはじめに、非同期畳み込みによるレンダリングとRealtime Databaseの監視部分を見てみましょう。
index.tsx
// 非同期畳み込みによる、レンダリング開始
Bacon.onValues(chatProperty, nameProperty, contentProperty,
(messages: List<IMessage>, name: string, content: string) => {
const props = { chatAction, messages, nameAction, name, contentAction, content };
ReactDOM.render(<App {...props} />, document.getElementById("content"));
});
// レンダリングが開始されてから、Realtime Databaseの
// 監視を開始。
chatRef.on("child_added", (snapShot) => {
const message: IMessage = snapShot.val();
chatAction.pushMessage(message);
});
このとき、レンダリングとRealtime DatabaseのObserveの順番が非常に大事です。もしこの順番が逆であったならば、コールバック関数の中でActionが先に呼び出されてしまい、データベースの初期値がコンポーネントに反映されなくなってしまいます。ChatActionの内容を見る前に、フォーム部分の処理を先に見てみましょう。
App.tsx
export default class App extends React.Component<IAppProps, {}> {
constructor(props) {
super(props);
}
render() {
const {messages, chatAction, name, nameAction, content, contentAction} = this.props;
return <div>
<input value={name} onChange={(e: any) =>
nameAction.changeName((e.target as HTMLInputElement).value)
} />
<input
value={content}
onChange={(e: any) =>
contentAction.changeContent((e.target as HTMLInputElement).value)
}
onKeyPress={(e: any) => {
const keyEvent = e as KeyboardEvent;
const currentContent: string = (keyEvent.target as HTMLInputElement).value;
if ((keyEvent.which === 13 || keyEvent.keyCode === 13) && currentContent !== "") {
keyEvent.preventDefault();
chatAction.pushCouldMessage({ name, content: currentContent });
contentAction.changeContent("");
}
} } />
<ul>
{messages.reverse().map((message, idx) =>
<li key={idx}>
<p>{`${message.name}: ${message.content}`}</p>
</li>
)}
</ul>
</div>;
}
}
今回一番大事なポイントは、たった一行です。
chatAction.pushCouldMessage({ name, content: currentContent });
おや、先程と呼び出すActionが違いますね?これがFirebaseをReactと組み合わせた場合の、ポイントの一つです。その謎を解き明かすために、ChatActionを見てみましょう。
ChatAction.ts
// ...
// Bacon.jsへのバインド部分は省略
private _pushMessage(oldMessages: List<IMessage>, message: IMessage): List<IMessage> {
return oldMessages.push(message);
}
private _pushCloudMessage(oldMessages: List<IMessage>, message: IMessage): List<IMessage> {
this.chatRef.push(message);
return oldMessages;
}
ネタが分かってしまえば、単純ですね。pushMessage Actionは、アプリケーションが持つチャットのメッセージリストを更新します。pushCloudMessage Actionは、chatRefを通じて、Realtime Databaseを更新させます。しかし、アプリケーションが持つメッセージリストの更新は行いません。まとめると、
- Realtime DatabaseのObserverが、チャットリストを更新
- メッセージ入力フォームは、Realtime Databaseを更新
としているということですね。これをしないと、自分の打ったメッセージが画面上で重複してしまうためです。
まとめ
今回の例は、最低限の小規模実装のため単純に同じことを再現しようとした場合、Firebaseが強力故にFirebase + jQuery辺りが一番短く実装出来ると思います。しかし、徐々に大規模にしていった場合やはりReact等のフレームワークの力が欲しくなってくると思います。そのときに考え無しに、ReactにFirebaseを組み合わせようとすると非常に複雑になってしまいます。FRPを使い、少し工夫するだけで複雑さを多少回避することが出来ます。現状の解決法を記事にはしましたが、当人は完全に納得はしていません。もし、もっと素晴らしい解決法があれば是非教えてください。それでは、良いFRPライフを!