はじめに
前回の記事までで、データの書き込みまでができました。
次にFirestoreに書き込んだデータの読み取りをやっていきます!
Firestoreではデータが双方向通信の仕組みがあるので、他のユーザーがデータベースの更新を行うのと同時にデータの再読み込みが行えるので、リアルタイムチャットが可能になります!
- 環境構築編
- Firebaseプロジェクト設定編
- データ書き込み編
- データ読み取り編 ← 本記事
全4回をかけて、簡単なチャットアプリを開発してきました!
今回の記事でアプリは完成します!
↓↓完成イメージです(再掲)
Firestoreからメッセージをリアルタイムで取得する
firebase関連のコードはlib/firebase.tsに記述したいのですが、簡略化のためscreens/ChatScreen.tsxに記述します。
useEffect
は、指定した変数を監視して、変数が更新した際の関数の実行を行うための関数です。
以下のコードでは、useEffect(() => {getMessages()}, [])
としており、第二引数に何も指定しない場合、この画面がマウントされたときのみ関数を実行してくれます。
今回は、メッセージの変更や削除は行えない仕様なので、データの追加が行われたときのみ、messagesの更新を行うようにしています。
FlatList
のinverted
というプロパティをtrue
にすることにより、FlatList
を反転させ、よくあるチャットアプリのように、リストの下側にメッセージを随時追加できるようにしています。
また、入力範囲がキーボードで隠れてしまわないように、画面全体をKeyboardAvoidingView
で囲っています。
//screens/ChatScreen.tsx
import React, { useState, useEffect } from 'react';
import {
StyleSheet,
TextInput,
SafeAreaView,
KeyboardAvoidingView,
View,
Text,
Button,
FlatList,
Alert
} from 'react-native';
import { StatusBar as ExpoStatusBar } from 'expo-status-bar';
import firebase from 'firebase';
import { getMessageDocRef } from '../lib/firebase';
import { Message } from '../types/message';
export const ChatScreen = () => {
const [text, setText] = useState<string>('');
const [messages, setMessages] = useState<Message[]>([]);
const sendMessage = async (value: string) => {
if (value != '') {
const docRef = await getMessageDocRef();
const newMessage = {
text: value,
createdAt: firebase.firestore.Timestamp.now(),
userId: ''
} as Message;
await docRef.set(newMessage);
setText('');
} else {
Alert.alert('エラー', 'メッセージを入力してください');
}
};
const getMessages = async () => {
const messages = [] as Message[];
await firebase
.firestore()
.collection('messages')
.orderBy('createdAt', 'desc')
.onSnapshot((snapshot) => {
snapshot.docChanges().forEach((change) => {
if (change.type === 'added') {
messages.push(change.doc.data() as Message);
}
setMessages(messages);
});
});
};
useEffect(() => {
getMessages();
}, []);
return (
<SafeAreaView style={styles.container}>
<ExpoStatusBar style="light" />
<KeyboardAvoidingView behavior="padding">
<FlatList
style={styles.messagesContainer}
data={messages}
inverted={true}
renderItem={({ item }: { item: Message }) => (
<Text style={{ color: '#fff' }}>{item.text}</Text>
)}
keyExtractor={(_, index) => index.toString()}
/>
<View style={styles.inputTextContainer}>
<TextInput
style={styles.inputText}
onChangeText={(value) => {
setText(value);
}}
value={text}
placeholder="メッセージを入力してください"
placeholderTextColor="#777"
autoCapitalize="none"
autoCorrect={false}
returnKeyType="done"
/>
<Button
title="send"
onPress={() => {
sendMessage(text);
}}
/>
</View>
</KeyboardAvoidingView>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#333',
alignItems: 'center',
justifyContent: 'center'
},
messagesContainer: {
width: '100%',
padding: 10
},
inputTextContainer: {
width: '100%',
flexDirection: 'row',
alignItems: 'center'
},
inputText: {
color: '#fff',
borderWidth: 1,
borderColor: '#999',
height: 32,
flex: 1,
borderRadius: 5,
paddingHorizontal: 10
}
});
これで前回の記事でFirestoreに追加したテキストが表示されるようになるはずです。
新しくメッセージを送信すると、すぐに画面に反映されます。
しかし、このままだと、自分が送ったメッセージなのか他人が送ったメッセージなのかがわからないので、どんどん改修していきます。
匿名ログインをする
まずユーザー情報をFirestoreに登録するために、匿名ログインの機能を追加します。
前々回に Firebase Authentication の設定は済んでいるので、ここではコードの実装のみを取り扱います。
lib/firebase.tsにログインのための関数を書いていきます。
//lib/firebase
import * as firebase from 'firebase';
import 'firebase/firestore';
import 'firebase/auth'
const firebaseConfig = {
apiKey: 'YOUR_API_KEY',
authDomain: 'YOUR_AUTH_DOMAIN',
databaseURL: 'YOUR_DATABASE_URL',
projectId: 'YOUR_PROJECT_ID',
storageBucket: 'YOUR_STORAGE_BUCKET',
messagingSenderId: 'YOUR_MESSAGING_SENDER_ID',
appId: 'YOUR_APP_ID'
};
if (!firebase.apps.length) {
firebase.initializeApp(firebaseConfig);
}
export const getMessageDocRef = async () => {
return await firebase.firestore().collection('messages').doc();
};
export const getUserId = async () => {
const userCredential = await firebase.auth().signInAnonymously();
return userCredential.user?.uid;
};
screens/ChatScreen.tsxも編集します。
useEffect
でマウント時に匿名ログインをするように処理を追加します。
また、メッセージの送信時にユーザーIDの情報も付与して送信するように変更しています。
//screens/ChatScreen.tsx
import React, { useState, useEffect } from 'react';
import {
StyleSheet,
TextInput,
SafeAreaView,
KeyboardAvoidingView,
View,
Text,
Button,
FlatList,
Alert
} from 'react-native';
import { StatusBar as ExpoStatusBar } from 'expo-status-bar';
import firebase from 'firebase';
import { getMessageDocRef, getUserId } from '../lib/firebase';
import { Message } from '../types/message';
export const ChatScreen = () => {
const [text, setText] = useState<string>('');
const [messages, setMessages] = useState<Message[]>([]);
const [userId, setUserId] = useState<string | undefined>();
const sendMessage = async (value: string, uid: string | undefined) => {
if (value != '') {
const docRef = await getMessageDocRef();
const newMessage = {
text: value,
createdAt: firebase.firestore.Timestamp.now(),
userId: uid
} as Message;
await docRef.set(newMessage);
setText('');
} else {
Alert.alert('エラー', 'メッセージを入力してください');
}
};
const getMessages = async () => {
const messages = [] as Message[];
await firebase
.firestore()
.collection('messages')
.orderBy('createdAt')
.onSnapshot((snapshot) => {
snapshot.docChanges().forEach((change) => {
if (change.type === 'added') {
messages.unshift(change.doc.data() as Message);
}
});
setMessages(messages);
});
};
const signin = async () => {
const uid = await getUserId();
setUserId(uid);
};
useEffect(() => {
signin();
getMessages();
}, []);
return (
<SafeAreaView style={styles.container}>
<ExpoStatusBar style="light" />
<KeyboardAvoidingView behavior="padding">
<FlatList
style={styles.messagesContainer}
data={messages}
inverted={true}
renderItem={({ item }: { item: Message }) => (
<Text style={{ color: '#fff' }}>{item.text}</Text>
)}
keyExtractor={(_, index) => index.toString()}
/>
<View style={styles.inputTextContainer}>
<TextInput
style={styles.inputText}
onChangeText={(value) => {
setText(value);
}}
value={text}
placeholder="メッセージを入力してください"
placeholderTextColor="#777"
autoCapitalize="none"
autoCorrect={false}
returnKeyType="done"
/>
<Button
title="send"
onPress={() => {
sendMessage(text, userId);
}}
/>
</View>
</KeyboardAvoidingView>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#333',
alignItems: 'center',
justifyContent: 'center'
},
messagesContainer: {
width: '100%',
padding: 10
},
inputTextContainer: {
width: '100%',
flexDirection: 'row',
alignItems: 'center'
},
inputText: {
color: '#fff',
borderWidth: 1,
borderColor: '#999',
height: 32,
flex: 1,
borderRadius: 5,
paddingHorizontal: 10
}
});
アプリを再起動して、メッセージを送ってみます。
Authentication を確認すると、一人ユーザーが追加されています。
また、Firestore を確認すると、しっかりドキュメントが userId のデータを持っています。
送信したメッセージと受信したメッセージで表示を変える
components/MessageItem.tsxを作成します。
//components/MessageItem.tsx
import React from 'react';
import { View, Text } from 'react-native';
import { Message } from '../types/message';
type Props = {
userId: string | undefined;
item: Message;
};
export const MessageItem: React.FC<Props> = ({ item, userId }: Props) => {
return (
<View
style={
userId == item.userId
? {
alignSelf: 'flex-end',
backgroundColor: '#007AFF',
padding: 5,
borderRadius: 5,
borderTopRightRadius: 0,
marginBottom: 5
}
: {
alignSelf: 'flex-start',
backgroundColor: '#fff',
padding: 5,
borderRadius: 5,
borderTopLeftRadius: 0,
marginBottom: 5
}
}
>
<Text style={userId == item.userId ? { color: '#fff' } : {}}>
{item.text}
</Text>
</View>
);
};
そして、screens/ChatScreen.tsxのFlatList
のrenderItem
に上記のコンポーネントを渡します。
また、Androidのノッチを避ける処理や、ボタンの色など微調整を加えています。
//screens/ChatScreen.tsx
import React, { useState, useEffect } from 'react';
import {
StyleSheet,
TextInput,
SafeAreaView,
KeyboardAvoidingView,
View,
Button,
FlatList,
Alert,
Platform,
StatusBar
} from 'react-native';
import { StatusBar as ExpoStatusBar } from 'expo-status-bar';
import firebase from 'firebase';
import { getMessageDocRef, getUserId } from '../lib/firebase';
import { Message } from '../types/message';
import { MessageItem } from '../components/MessageItem';
export const ChatScreen = () => {
const [text, setText] = useState<string>('');
const [messages, setMessages] = useState<Message[]>([]);
const [userId, setUserId] = useState<string | undefined>();
const sendMessage = async (value: string, uid: string | undefined) => {
if (value != '') {
const docRef = await getMessageDocRef();
const newMessage = {
text: value,
createdAt: firebase.firestore.Timestamp.now(),
userId: uid
} as Message;
await docRef.set(newMessage);
setText('');
} else {
Alert.alert('エラー', 'メッセージを入力してください');
}
};
const getMessages = async () => {
const messages = [] as Message[];
await firebase
.firestore()
.collection('messages')
.orderBy('createdAt')
.onSnapshot((snapshot) => {
snapshot.docChanges().forEach((change) => {
if (change.type === 'added') {
messages.unshift(change.doc.data() as Message);
}
});
setMessages(messages);
});
};
const signin = async () => {
const uid = await getUserId();
setUserId(uid);
};
useEffect(() => {
signin();
getMessages();
}, []);
return (
<SafeAreaView style={styles.container}>
<ExpoStatusBar style="light" />
<KeyboardAvoidingView
style={styles.keyboardAvoidingView}
behavior="padding"
>
<FlatList
style={styles.messagesContainer}
data={messages}
inverted={true}
renderItem={({ item }: { item: Message }) => (
<MessageItem userId={userId} item={item} />
)}
keyExtractor={(_, index) => index.toString()}
/>
<View style={styles.inputTextContainer}>
<TextInput
style={styles.inputText}
onChangeText={(value) => {
setText(value);
}}
value={text}
placeholder="メッセージを入力してください"
placeholderTextColor="#777"
autoCapitalize="none"
autoCorrect={false}
returnKeyType="done"
/>
<Button
title="send"
color="#007AFF"
onPress={() => {
sendMessage(text, userId);
}}
/>
</View>
</KeyboardAvoidingView>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#333',
alignItems: 'center',
justifyContent: 'center',
paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0
},
keyboardAvoidingView: {
width: '100%',
flex: 1
},
messagesContainer: {
width: '100%',
padding: 10
},
inputTextContainer: {
width: '100%',
flexDirection: 'row',
alignItems: 'center',
padding: 10
},
inputText: {
color: '#fff',
borderWidth: 1,
borderColor: '#999',
height: 32,
flex: 1,
borderRadius: 5,
paddingHorizontal: 10
}
});
完成です!!
セキュリティルールの修正
最後に、Firestoreのセキュリティルールの修正を行っておきます。
第2回のFirebaseプロジェクト設定編にて、Firestoreをテストモードで開始しました。
テストモードでは、アプリの作成から1ヶ月間は誰でも読み取り、書き込みができるというルールになっています。
これでは11月14日以降、Firestoreにアクセスができなくなってしまうので、セキュリティルールを変更します。
ログインしたユーザーのみが読み取りと書き込みを行えるようにしたいので、以下のように設定します。
これで11月14日以降もFirestoreにアクセスできるようになりました。
さいごに
これを拡張して、ユーザー情報をもっと追加したり、ルームIDを設けるなどして、より実用的なチャットアプリができると思います!!
この記事をご覧いただいた React Native ユーザーが素晴らしいチャットアプリを開発してくれることを切に願うばかりです。