React Native + Firebase で超簡単なチャットアプリを作ってみた‼️ (データ読み込み編【完成】)

# はじめに
**[前回の記事](https://qiita.com/shintaroa/items/4268c0cc2e10a49f4fce)**までで、データの書き込みまでができました。
次にFirestoreに書き込んだデータの読み取りをやっていきます！
Firestoreではデータが**双方向通信**の仕組みがあるので、他のユーザーがデータベースの更新を行うのと同時にデータの再読み込みが行えるので、リアルタイムチャットが可能になります！

1. [環境構築編](https://qiita.com/shintaroa/items/ccd0ebe0b41fdce34565)
2. [Firebaseプロジェクト設定編](https://qiita.com/shintaroa/items/b69362129407c628df07)
3. [データ書き込み編](https://qiita.com/shintaroa/items/4268c0cc2e10a49f4fce)
4. **データ読み取り編** ← 本記事

全4回をかけて、簡単なチャットアプリを開発してきました！
今回の記事でアプリは完成します！
↓↓完成イメージです:bangbang:(再掲)

<img width=300 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316856/4707a209-4275-65c4-71b6-38eef867c751.png" />

# Firestoreからメッセージをリアルタイムで取得する

firebase関連のコードはlib/firebase.tsに記述したいのですが、簡略化のためscreens/ChatScreen.tsxに記述します。

`useEffect`は、指定した変数を監視して、変数が更新した際の関数の実行を行うための関数です。
以下のコードでは、`useEffect(() => {getMessages()}, [])`としており、第二引数に何も指定しない場合、この画面がマウントされたときのみ関数を実行してくれます。

今回は、メッセージの変更や削除は行えない仕様なので、データの追加が行われたときのみ、messagesの更新を行うようにしています。

`FlatList`の`inverted`というプロパティを`true`にすることにより、`FlatList`を反転させ、よくあるチャットアプリのように、リストの下側にメッセージを随時追加できるようにしています。

また、入力範囲がキーボードで隠れてしまわないように、画面全体を`KeyboardAvoidingView`で囲っています。

```tsx
//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に追加したテキストが表示されるようになるはずです。

<img width=300 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316856/bd68cdf4-b1e2-4c9b-a990-bfa915e8f824.png" />

新しくメッセージを送信すると、すぐに画面に反映されます。

<img width=300 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316856/4b043473-a445-5dfb-4bcb-7db5fd44f976.png" />

しかし、このままだと、**自分が送ったメッセージなのか他人が送ったメッセージなのかがわからない**ので、どんどん改修していきます。

# 匿名ログインをする

まずユーザー情報をFirestoreに登録するために、匿名ログインの機能を追加します。
前々回に Firebase Authentication の設定は済んでいるので、ここではコードの実装のみを取り扱います。

lib/firebase.tsにログインのための関数を書いていきます。

```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の情報も付与して送信するように変更しています。

```tsx
//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
    }
});
```

アプリを再起動して、メッセージを送ってみます。

<img width=300 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316856/6cb98351-5076-0f7c-6d1c-59ba08474f9f.png" />

Authentication を確認すると、一人ユーザーが追加されています。

![スクリーンショット 2020-10-14 20.39.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316856/dbe5b757-f33a-9dba-7703-72ca63d70229.png)

また、Firestore を確認すると、しっかりドキュメントが userId のデータを持っています。

![スクリーンショット 2020-10-14 20.39.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316856/29762cad-957e-4a9d-74d7-8e7295880651.png)

# 送信したメッセージと受信したメッセージで表示を変える

components/MessageItem.tsxを作成します。

![スクリーンショット 2020-10-14 21.02.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316856/370ec983-bb6a-b9fd-d43a-2d0cbce6042d.png)

```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のノッチを避ける処理や、ボタンの色など微調整を加えています。

```tsx
//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
    }
});
```

完成です！！

<img width=300 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316856/4707a209-4275-65c4-71b6-38eef867c751.png" />

# セキュリティルールの修正
最後に、Firestoreのセキュリティルールの修正を行っておきます。
第2回のFirebaseプロジェクト設定編にて、Firestoreをテストモードで開始しました。
テストモードでは、アプリの作成から1ヶ月間は誰でも読み取り、書き込みができるというルールになっています。
![スクリーンショット 2020-10-15 3.19.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316856/4dd38626-b4e6-8018-5ce3-d8b964f5425d.png)
これでは11月14日以降、Firestoreにアクセスができなくなってしまうので、セキュリティルールを変更します。
ログインしたユーザーのみが読み取りと書き込みを行えるようにしたいので、以下のように設定します。
![スクリーンショット 2020-10-15 3.19.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316856/b7545f71-bdb9-8b1f-b983-d4dace4b1be4.png)
これで11月14日以降もFirestoreにアクセスできるようになりました。

# さいごに
これを拡張して、ユーザー情報をもっと追加したり、ルームIDを設けるなどして、より実用的なチャットアプリができると思います！！
この記事をご覧いただいた React Native ユーザーが素晴らしいチャットアプリを開発してくれることを切に願うばかりです。