Firebase Realtime Databaseを使ってChatのアプリを作りたいと思います 書き込みよりも難しいのは一覧取得でした。
この記事はReact Firebase入門シリーズです
1-1-1・ Firebase初期設定とFirebaseAuthのSignUp
1-1-2・ 「react-hook-form」を入れてみた
1-1-3・ AuthのSignUpの通信エラー対応
1-2 ・ FirebaseAuthのログイン処理
1-3 ・ FirebaseAuthのログイン認証とログアウト処理
1-4 ・ FirebaseAuthのパスワード初期化処理
1-5 ・ FirebaseAuthのメールアドレスの有効化
1-6 ・ FirebaseAuthのメールアドレスとパスワード変更
1-7 ・ FirebaseAuthの表示名変更
1-8 ・ FirebaseAuthの拡張項目追加
1-9-1 ・ FirebaseAuthのTwitter認証
1-9-2 ・ FirebaseAuthのTwitter認証(既存ユーザー向け)と解除
1-10 ・ FirebaseAuthのGoogle認証(既存ユーザー含む)と解除
2-1・ FirebaseStorageのファイルアップ:基礎
2-2・ FirebaseStorageのファイルアップ前に画像の切り抜き
2-3・ FirebaseStorageのファイルアップの移動
3-1・FirestoreのCRUD
3-2・Firestoreのデータ取得補足
3-3・Firestoreのページネーション処理
3-4・Firestoreのコレクション(テーブル)のJOIN
4-1・Realtime Databaseでchatアプリ(書き込み)
4-2・Realtime Databaseでchatアプリ(一覧取得) 今ここ
5・ FirebaseのHosting(デプロイ)
6-1・ Cloud FunctionsでHello,world
6-2・ Cloud Functions(エミュレータ)でHello,world
6-3・ ユーザーの作成時にCloud Functionsを実行
6-4・ Cloud Functionsでメール送信
6-5・ Cloud Functionsでslackに通知する
やりたい事
- chatアプリの作成
- Realtimedatabaseの一覧取得
mysqlなら楽勝のレベルですが、他の人が更新に反映する部分がちょっと面倒くさいです
プログラム
ルーティングの設定
chatの一覧取得のルーティングを追加します
import {Index as chatIndex } from "./chat/Index" function App() { return ( <> <Router> <AuthProvider> <Switch> ・ ・ <AuthFirebaseRoute path="/chat/index" component={chatIndex} /> ・ </Switch> </AuthProvider> </Router> </> ); }
このプログラムは書き込み時にユーザーも書き込みのでAuthの情報が必要です。
Chatのデータ取得プログラム
import {useMemo,useState, useEffect} from 'react' import {database} from "../../firebase" // カスタムフックにしておく const useDatabase = () => { // 同じパスでは毎回同じ結果が得られるのでmemo化しておく return useMemo(() => database.ref('/messages').orderByChild('createAt').limitToLast(30), []); }; // hooksを使いたいのでカスタムhooksにしておく const useFetchData = (ref) => { const [data, setData] = useState(); useEffect(() => { // イベントリスナーを追加するにはonを使う ref.on('value', snapshot => { // パスに対する全データを含むsnapshotが渡される // ない場合はnullが変えるので存在をチェックしておく if (snapshot?.val()) { console.log("snapshot.val()") setData(snapshot.val()); } }); return () => { ref.off(); }; // refの変更に応じて再取得する }, [ref]); // データを返却する return { data }; } // 実際に呼び出す際はこちらを使う export const useFetchAllData = () => { // refを取得して const ref = useDatabase(); // ref渡してデータを取得する return useFetchData(ref); };
- このプログラムは以下の記事を参考にしてつくりました
イベントリスナーを追加するにはonの部分が更新時に反映してくれる部分になります。
useMemoを使って無駄なRenderingを減らしています
orderByChildを使っているので下記のWARNINGが発生します
FIREBASE WARNING: Using an unspecified index. Your data will be downloaded and filtered on the client. Consider adding ".indexOn": "createAt" at /messages to your security rules for better performance.
回避するためにルールの部分にインデックスを貼ります
{ "rules": { ".read": "true", ".write": "auth != null", "messages": { ".indexOn": ["createAt"] }, } }
ついでにWriteのセキュリティーのルールも変えます。 この設定でエラーが消えます。
メッセージ単体
メッセージデータをもらって出力するだけのコンポーネントです。 AUTH関係のContextのデータも使っていないので単純なfunctionコンポーネントです。CSSの部分が長い
import React , { useEffect, useRef } from 'react' import { createStyles, makeStyles, Theme } from "@material-ui/core/styles"; import Avatar from '@material-ui/core/Avatar'; import { deepOrange } from '@material-ui/core/colors'; const useStyles = makeStyles((theme: Theme) => createStyles({ messageRow: { display: "flex", }, messageRowRight: { display: "flex", justifyContent: "flex-end" }, messageBlue: { position: "relative", marginLeft: "20px", padding: "10px", backgroundColor: "#A8DDFD", marginBottom: "10px", width: "60%", minWidth: "30%", //minHeight: "50px", textAlign: "left", font: "400 .9em 'Open Sans', sans-serif", border: "1px solid #97C6E3", borderRadius: "10px", '&:after': { content: "''", position: "absolute", width: "0", height: "0", borderTop: "15px solid #A8DDFD", borderLeft: "15px solid transparent", borderRight: "15px solid transparent", top: "0", left: "-15px", }, '&:before': { content: "''", position: "absolute", width: "0", height: "0", borderTop: "17px solid #97C6E3", borderLeft: "16px solid transparent", borderRight: "16px solid transparent", top: "-1px", left: "-17px", }, }, messageOrange: { position: "relative", marginRight: "20px", marginBottom: "10px", padding: "10px", backgroundColor: "#f8e896", width: "60%", //height: "50px", textAlign: "left", font: "400 .9em 'Open Sans', sans-serif", border: "1px solid #dfd087", borderRadius: "10px", '&:after': { content: "''", position: "absolute", width: "0", height: "0", borderTop: "15px solid #f8e896", borderLeft: "15px solid transparent", borderRight: "15px solid transparent", top: "0", right: "-15px", }, '&:before': { content: "''", position: "absolute", width: "0", height: "0", borderTop: "17px solid #dfd087", borderLeft: "16px solid transparent", borderRight: "16px solid transparent", top: "-1px", right: "-17px", }, }, messageContent: { padding: 0, margin: 0, }, messageTimeStampRight: { position: "absolute", fontSize: ".85em", fontWeight: "300", marginTop: "10px", bottom: "-3px", right: "5px", }, orange: { color: theme.palette.getContrastText(deepOrange[500]), backgroundColor: deepOrange[500], width: theme.spacing(4), height: theme.spacing(4), }, avatarNothing: { color: "transparent", backgroundColor: "transparent", width: theme.spacing(4), height: theme.spacing(4), }, displayName: { marginLeft: "20px", }, }) ); //avatarが左にあるメッセージ(他人) export const MessageLeft = (props) => { const message = props.message ? props.message : 'no message' ; const timestamp = props.timestamp ? props.timestamp : '' ; const photoURL = props.photoURL ? props.photoURL : 'dummy.js' ; const displayName = props.displayName ? props.displayName : '名無しさん' ; const classes = useStyles(); return ( <div className={classes.messageRow}> <Avatar alt={displayName} className={classes.orange} src={photoURL} ></Avatar> <div> <div className={classes.displayName}>{displayName}</div> <div className={classes.messageBlue}> <div><p className={classes.messageContent}>{ message}</p></div> <div className={classes.messageTimeStampRight}>{timestamp}</div> </div> </div> </div> ) } //自分のメッセージ export const MessageRight = (props) => { const classes = useStyles(); const message = props.message ? props.message : 'no message' ; const timestamp = props.timestamp ? props.timestamp : '' ; const messagesEndRef = useRef(null) const scrollToBottom = () => { messagesEndRef.current.scrollIntoView({ behavior: "smooth" }) } useEffect(scrollToBottom, [props]); return ( <> <div className={classes.messageRowRight}> <div className={classes.messageOrange}> <p className={classes.messageContent}>{ message}</p> <div className={classes.messageTimeStampRight}>{timestamp}</div> </div> </div> <div ref={messagesEndRef} /> </> ) }
単体の部分だとこんな感じになります
他人のメッセージの場合
自分の場合はメッセージ
データが入った時にスクロールするようにしています。いらない場合はこの処理を消す形になる
const messagesEndRef = useRef(null) const scrollToBottom = () => { messagesEndRef.current.scrollIntoView({ behavior: "smooth" }) }
Chatの一覧プログラム
import React ,{useMemo} from 'react' import { createStyles, makeStyles, Theme } from "@material-ui/core/styles"; import { Paper, } from '@material-ui/core'; import {TextInput} from './TextInput.js'; import {MessageLeft,MessageRight} from './Message'; import { useFetchAllData } from './firebaseDB'; import { useAuth } from "../../contexts/AuthContext" import moment from 'moment'; const useStyles = makeStyles((theme: Theme) => createStyles({ paper: { width: '80vw', height: '80vh', maxWidth: '500px', maxHeight: '700px', display: 'flex', alignItems: 'center', flexDirection: 'column', position: 'relative' }, paper2: { width: '80vw', maxWidth: '500px', display: 'flex', alignItems: 'center', flexDirection: 'column', position: 'relative' }, container: { width: '100vw', height: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center' }, messagesBody: { width: 'calc( 100% - 20px )', margin: 10, overflowY: 'scroll', height: 'calc( 100% - 80px )' }, }) ); export const Index = () => { const classes = useStyles(); const { currentUser} = useAuth() //Firebaseの共通変数と変更などの関数 const { data } = useFetchAllData(); // object形式なので使いやすいように{key, value}形式のリストに変換する // また、データが変わらない限り結果は同じなのでメモ化しておく const dataList = useMemo(() => Object.entries(data || {}).map(([key, value]) => ({ key, value })), [data]); console.log(dataList) let viewdate = ''; return ( <div className={classes.container}> <Paper className={classes.paper} > <Paper id="style-1" className={classes.messagesBody}> {dataList.length === 0 && "loading..."} {dataList.map(({ key, value }) => <React.Fragment key={`${key}`}> { //日付がちがった場合は日付を出力する(jsxだと単純にif文をかけないのでアロー関数で逃げる) (() => { if (viewdate !== moment(value.createAt).format('YYYY/MM/DD')) { viewdate = moment(value.createAt).format('YYYY/MM/DD') return(viewdate); } })() } { currentUser.uid !== value.uid && <MessageLeft message={value.message + ' '} timestamp={moment(value.createAt).format('HH:mm') } photoURL={value.photoURL} displayName={value.displayName} /> } { currentUser.uid === value.uid && <MessageRight message={value.message + ' '} timestamp={moment(value.createAt).format('HH:mm') } photoURL={value.photoURL} displayName={value.displayName} /> } </React.Fragment> )} </Paper> <TextInput /> </Paper> </div> ) }
プログラムの補足
- JSXは普通にIFをかけないのでアロー関数で書いています。ちょっと面倒くさい
{ //日付がちがった場合は日付を出力する(jsxだと単純にif文をかけないのでアロー関数で逃げる) (() => { if (viewdate !== moment(value.createAt).format('YYYY/MM/DD')) { viewdate = moment(value.createAt).format('YYYY/MM/DD') return(viewdate); } })() }
currentUser.uid === value.uid は自分のメッセージは右からのコンポーネントを呼ぶために切り替えています。メッセージのコンポーネント側で切り替えてもよかった気がする。
{dataList.length === 0 && "loading..."} はRealTime Databaseは初期の読み込みに時間がかかるのでLoadingの文字を出力しています
message={value.message + ' '} は半角だけのメッセージだと枠におさまらないという問題があったので全角スペースを足すことでさけました。CSSの力があればこんな作りにならないですが・・・
結果
できていない事
- テキストしか入力できないです。LINEだと画像とかスタンプとか入れれます。作りたいけど難しい
感想
- データ取得部分は丸パクリなので、理解が浅いのもう少し理解を深めたい