In this tutorial, the first thing you'll learn is how to use chat messages stored in a Firestore database. The second thing you're going to learn in this post is how to integrate and use react-native-gifted-chat to implement Chat UI with ease.

Please make sure you go through the previous post in this series here to learn more about the requirements and what libraries or npm packages this app is already using.

Adding Firestore to a React Native app

To use the Firestore database, all you have to do is install the @react-native-firebase/firestore package and build the app again.

Open up a terminal window and execute the following command:

yarn add @react-native-firebase/firestore

# do not forget to install pods for ios
cd ios / && pod install

# after pods have been installed
cd ..

Do note that the Firestore package from react-native-firebase depends on two other packages:

  • @react-native-firebase/app
  • @react-native-firebase/auth

This means that you must install these two packages to use Firestore as well.

The last step in this section is to rebuild the app for each OS.

# for iOS
npx react-native run-ios

# for Android

npx react-native run-android

Storing the chat room name

To store the chat room name, open screens/CreateChatRoom.js and import the Firestore instance from its module after other import statements.

import firestore from '@react-native-firebase/firestore'

Then, in the helper method handleButtonPress, add the business logic to store the name of the chat room under the collection MESSAGE_THREADS. The unique id for each chat room is going to be created by Firestore itself.

The latestMessage field is an object that is going to add and display the first message for each chat room.

The createdAt field will act as the time stamp for the latest message. This field is also going to help later in the app, to sort the messages according to the latest message received.

Finally, when the chat room is created, you want to be able to navigate back to the ChatRoom or the previous screen. To add this, you are going to access the prop: navigation.

export default function CreateChatRoom({ navigation }) {
  // ... rest remains same
  function handleButtonPress() {
    if (roomName.length > 0) {
      // create new thread using firebase & firestore
      firestore()
        .collection('MESSAGE_THREADS')
        .add({
          name: roomName,
          latestMessage: {
            text: `${roomName} created. Welcome!`,
            createdAt: new Date().getTime()
          }
        })
        .then(() => {
          navigation.navigate('ChatRoom')
        })
    }
  }
}

Go back to the simulator and create a new chat room.

The chat room creation tool now has a Back button

Once the room has been created, you can verify it by going to the Firestore dashboard as shown below:

The Firestore dashboard has logged the new chat room

Display the message thread from Firestore

The Firestore database is currently integrated with the React Native app and is storing the new message threads that are being created. In this section, let us modify screens/ChatRoom.js to display these message threads in the form of a list.

Start by importing the following statements:

import React, { useState, useEffect } from 'react'
import {
  View,
  StyleSheet,
  Text,
  FlatList,
  TouchableOpacity,
  ActivityIndicator
} from 'react-native'
import firestore from '@react-native-firebase/firestore'
import Separator from '../components/Separator'

In the functional component ChatRoom, start by declaring a state variable called threads using the useReact hook. This state variable has a default value of an empty array and is going to fetch and display any incoming documents from the collection MESSAGE_THREADS from Firestore.

When the component loads, to fetch the existing message threads (in other words, to read the data from the Firestore), start by declaring a listener to the query. This listener is going to subscribe to updates. The updates can be a new message thread or an unread message in real-time. Declaring a listener here is also important since when the screen unmounts, it is important to unsubscribe from this listener.

Using the querySnapShot, each document of a message thread is going to be added in the state variable array threads. At this point, all data is returned from the query as well as a default object that contains the _id (required as unique for the FlatList component) and empty name and latestMessage.text fields.

There is also a loading state variable declared that is going to be set to false when the documents or messages are fetched from the Firestore.

export default function ChatRoom() {
  const [threads, setThreads] = useState([])
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    const unsubscribe = firestore()
      .collection('MESSAGE_THREADS')
      .orderBy('latestMessage.createdAt', 'desc')
      .onSnapshot(querySnapshot => {
        const threads = querySnapshot.docs.map(documentSnapshot => {
          return {
            _id: documentSnapshot.id,
            name: '',
            latestMessage: { text: '' },
            ...documentSnapshot.data()
          }
        })

        setThreads(threads)
        console.log(threads)
        if (loading) {
          setLoading(false)
        }
      })

    return () => unsubscribe()
  }, [])

  if (loading) {
    return <ActivityIndicator size='large' color='#555' />
  }

  // ...
}

The following FlatList component is going to be returned from this functional component. Each item in the list is going to be displayed in the form of the thread name and the latest message in that thread. The items are going to be organized according to the timestamp associated with the latest message of a thread.

export default function ChatRoom() {
  // ... rest of the code remains the same

  return (
    <View style={styles.container}>
      <FlatList
        data={threads}
        keyExtractor={item => item._id}
        renderItem={({ item }) => (
          <TouchableOpacity onPress={() => alert('Open a message thread')}>
            <View style={styles.row}>
              <View style={styles.content}>
                <View style={styles.header}>
                  <Text style={styles.nameText}>{item.name}</Text>
                </View>
                <Text style={styles.contentText}>
                  {item.latestMessage.text.slice(0, 90)}
                </Text>
              </View>
            </View>
          </TouchableOpacity>
        )}
        ItemSeparatorComponent={() => <Separator />}
      />
    </View>
  )
}

Here are the corresponding styles for the above code snippet:

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#dee2eb'
  },
  title: {
    marginTop: 20,
    marginBottom: 30,
    fontSize: 28,
    fontWeight: '500'
  },
  row: {
    paddingRight: 10,
    paddingLeft: 5,
    paddingVertical: 5,
    flexDirection: 'row',
    alignItems: 'center'
  },
  content: {
    flexShrink: 1
  },
  header: {
    flexDirection: 'row'
  },
  nameText: {
    fontWeight: '600',
    fontSize: 18,
    color: '#000'
  },
  dateText: {},
  contentText: {
    color: '#949494',
    fontSize: 16,
    marginTop: 2
  }
})

Lastly, here is the code snippet for the components/Separator.js component:

import React from 'react'
import { View, StyleSheet } from 'react-native'

export default function Separator() {
  return <View style={styles.separator} />
}

const styles = StyleSheet.create({
  separator: {
    backgroundColor: '#555',
    height: 0.5,
    flex: 1
  }
})

Here is the output you are going to get on a device screen:

The app displays two separate chat thread with the latest message at the top

Integrating react-native-gifted-chat package

The module react-native-gifted-chat is an excellent open-source package in the React Native ecosystem. It allows us to integrate and implement Chat UI in React Native apps seamlessly.

To start, install the specific version as shown below if you are using this package for the first time.

yarn add react-native-gifted-chat@0.9.11

Then, create a new screen component file called screens/Messages.js. This functional component is going to display the UI for each message thread. You are required to include this screen component file inside the SignInStack navigator such that it is displayed whenever a message thread is opened by the user.

Open the file navigation/SignInStack.js and import the following statement.

// ... after other imports
import Messages from '../screens/Messages'

Then, at the bottom of the Stack.Navigator, add the Stack screen Messages:

<Stack.Screen
  name='Messages'
  component={Messages}
  options={{
    title: 'Messages'
  }}
/>

To navigate from the ChatRoom to Messages for a message thread, you have to add it to the onPress prop for each message thread rendered in the ChatRoom.js file. Open it and make sure to pass in the prop navigation to the functional component.

export default function ChatRoom({ navigation }) {
  // ...
}

Then, add the following value for the prop on TouchableOpacity.

<TouchableOpacity
  onPress={() => navigation.navigate('Messages', { thread: item })}>
  {/* ... */}
</TouchableOpacity>

Next, go the file screens/Messages.js and add mock chat threads to integrate react-native-gifted-chat. Import the following statements:

import React, { useState } from 'react'
import { GiftedChat } from 'react-native-gifted-chat'

Then, in the functional component Messages, define a state variable with some mock data that is going to be shown on the UI screen.

export default function Messages() {
  const [messages, setMessages] = useState([
    {
      _id: 0,
      text: 'thread created',
      createdAt: new Date().getTime(),
      system: true
    },
    {
      _id: 1,
      text: 'hello!',
      createdAt: new Date().getTime(),
      user: {
        _id: 2,
        name: 'Demo'
      }
    }
  ])

  // ...
}

Add a helper method that is going to be used when sending a message in a particular thread.

function handleSend(newMessage = []) {
  setMessages(GiftedChat.append(messages, newMessage))
}

Lastly, return the following code snippet. The newMessage is concatenated with the previous or initial messages using GiftedChat.append().

return (
  <GiftedChat
    messages={messages}
    onSend={newMessage => handleSend(newMessage)}
    user={{
      _id: 1
    }}
  />
)

Here is the output you are going to get after this step:

The chat thread renders messages in chronological order

Display thread name in the screen's title

When a user opens a thread in the app from the ChatRoom screen and navigates to a particular Message screen for that thread, in the header, the title just says Messages. Let us display the name of the thread instead. This can be done by using the route object in the stack navigator's options for the Messages screen. Open, SignInStack.js and modify the last stack screen as shown below:

<Stack.Screen
  name='Messages'
  component={Messages}
  options={({ route }) => ({
    title: route.params.thread.name
  })}
/>

The value of the title for each thread is going to be the name of that thread. This is possible because, in the previous section, when navigating from ChatRoom to Messages, you passed an object with the data stored in the Firestore as shown below.

navigation.navigate('Messages', { thread: item })

Here is the output you are going to get on the device:

The thread title is now displayed at the top of the screen

Create a nested collection to store each message

In this section, let us nest another collection inside the MESSAGE_THREADS collection in Firestore. We're doing this in order to store each message object based on their timestamp as well as which authorized user (based on their uid) is sending the message. The final output afer completing this section is going to look like the following image:

Firestore correctly stores the nested collection

In the below image, notice that a particular message sent by the user is going to have fields such as createdAt, text (which is static in the below image but is going to be dynamic later in this tutorial), and the user.uid.

Start by opening the file screens/CreateChatRoom.js and modify the promise returned inside the handleButtonPress function.

The modification is going to be done by passing the docRef as the parameter. A docRef or the DocumentReference in Firestore refers to the location in the database that is used for CRUD (read, write, and so on).

In the snippet below, modify the following:

.then(docRef => {
  docRef.collection('MESSAGES').add({
    text: `${roomName} created. Welcome!`,
    createdAt: new Date().getTime(),
    system: true
  })
  navigation.navigate('ChatRoom')
})

Now, open the file screens/Messages.js and start by importing the following statements:

import firestore from '@react-native-firebase/firestore'
import auth from '@react-native-firebase/auth'

To get the reference to the current thread screen, make sure to pass route as the parameter to the functional component Messages. Also, get the thread from the route.params object (this is provided by the react-navigation library) and the current user info from the auth() module.

const { thread } = route.params
const user = auth().currentUser.toJSON()

export default function Messages({ route }) {
  // ...
}

Next, inside the handleSend message, get the value of the field text.

async function handleSend(messages) {
  const text = messages[0].text
  // ...
}

To create the collection and the messages for the new collection, add the following to the handleSend method. Accessing the current thread _id is a must.

firestore()
  .collection('MESSAGE_THREADS')
  .doc(thread._id)
  .collection('MESSAGES')
  .add({
    text,
    createdAt: new Date().getTime(),
    user: {
      _id: user.uid,
      displayName: user.displayName
    }
  })

Then, make an asynchronous call to the Firestore to create a new document in the collection MESSAGE_THREADS by referring to the _id of the current thread.

await firestore()
  .collection('MESSAGE_THREADS')
  .doc(thread._id)
  .set(
    {
      latestMessage: {
        text,
        createdAt: new Date().getTime()
      }
    },
    { merge: true }
  )

In the above snippet, make sure you pass the second property merge whenever you are using set with Firestore.

Fetch real-time messages to display from the Firestore collection

To fetch the messages stored in the MESSAGES sub-collection for each thread, let us use the React hook useEffect. With the help of this hook, you will be able to listen and fetch the data from the Firestore as well as stop listening when the component unmounts. Open screens/Messages.js and modify the first import statement.

import React, { useState, useEffect } from 'react'

Then, define a useEffect hook whose callback is going to have a listener that fetches the data from the sub-collection and sets the data to state variable messages. At the end of this callback, make sure to stop listening to the updates as soon as the functional component unmounts.

useEffect(() => {
  const unsubscribeListener = firestore()
    .collection('MESSAGE_THREADS')
    .doc(thread._id)
    .collection('MESSAGES')
    .orderBy('createdAt', 'desc')
    .onSnapshot(querySnapshot => {
      const messages = querySnapshot.docs.map(doc => {
        const firebaseData = doc.data()

        const data = {
          _id: doc.id,
          text: '',
          createdAt: new Date().getTime(),
          ...firebaseData
        }

        if (!firebaseData.system) {
          data.user = {
            ...firebaseData.user,
            name: firebaseData.user.displayName
          }
        }

        return data
      })

      setMessages(messages)
    })

  return () => unsubscribeListener()
}, [])

Lastly, go to the return statement of this functional component and modify the GiftedChat props as below:

<GiftedChat
  messages={messages}
  onSend={handleSend}
  user={{
    _id: user.uid
  }}
/>

In the above snippet, the user.uid has to be fetched for the current user and cannot send the static value as before.

Here is the output. The app is being tested on two iOS simulators and each user has a different uid.

The two different users have different chat screens according to their submissions

Also, when you navigate back to the ChatRoom screen or the home screen in the current app scenario, the latest message is displayed.

The latest message is visible on the thread list
The latest message updates in real time

Conclusion

That's it for this tutorial. Congratulations! You have completely integrated different libraries to build a React Native Chat app. There are different ways that you can extend this application, but I'll leave that to your imagination.