undefined
undefined

Reputation: 441

Displaying firestore timestamp gets error when using onSnapshot() on react-native

I am creating a chat app using firestore where I use Flatlist and querying using onSnapshot() to show both sender and receiver realtime

my sample query below is like this:

const ChatMessages = useCallback(() => {
 privateMessage
  .doc(chatId)
  .collection('messages')
  .orderBy('createdAt', 'asc')
  .onSnapshot((querySnapshot) => {
    const messageList = [];
    querySnapshot.forEach((doc) => {
      messageList.push(doc.data());
    })
    setMessages(messageList);
  }, error => {
    console.log(error)
  })
 }, [chatId])

then in my sample flatlist I have the user display name, message, and the time created that will be displayed on the screen:

<FlatList
  data={messages}
  keyExtractor={(item, index) => String(index)}
  removeClippedSubviews={false}
  renderItem={({ item }) => (

   <View>

      <Text style={chatstyle.pmdate}>
        {item.createdAt.toDate().toDateString()}
      </Text>

      <Text>
        {item.displayName}
      </Text>

      <Text>
        {item.message}
      </Text>

   </View>
  )}
/>

I would like to display the time and date but when using onSnapshot() I get null is not an object (evaluating 'item.createdAt.toDate') error but when I removed {item.createdAt.toDate().toDateString()} everything is ok. I tried the get() query and the timestamp works fine but of course it's not realtime.

What is the right way to display the timestamp using onSnapshot()?

Upvotes: 0

Views: 847

Answers (2)

Sandrin Joy
Sandrin Joy

Reputation: 1169

I'm new to React as well, but for those who are looking a quick solution & couldn't figure out a way to do it , here is one :

const ChatMessages = useCallback(() => {
 privateMessage
  .doc(chatId)
  .collection('messages')
  .orderBy('createdAt', 'asc')
  .onSnapshot((querySnapshot) => {
    const messageList = [];
    querySnapshot.forEach((doc) => {
     if(querySnapshot.createdAt) // Here is the magic Line of Code
      messageList.push(doc.data());
    })
    setMessages(messageList);
  }, error => {
    console.log(error)
  })
 }, [chatId])

Yes that's it. A single if statement which checks for the createdAt attribute in the data & then only pushes the data to your array.

If you are thinking about the drawbacks of this solution, then here is it :

time for the if() to validate is extra in your program.

PS : I tried the current approved solution on my react application, but still the null issue on the timeStamp was happening.

Upvotes: 2

Renaud Tarnec
Renaud Tarnec

Reputation: 83058

Your problem comes from the "latency compensation": "Local writes in your app will invoke snapshot listeners immediately. ... When you perform a write, your listeners will be notified with the new data before the data is sent to the backend". See the doc on onSnapshot().

Since you use firebase.firestore.FieldValue.serverTimestamp() to set the value of createdAt (which is a good approach), the value of createdAt is calculated by the backend (the serverTimestamp sentinel is replaced by a server-generated timestamp in the written data).

Therefore, at the moment the snapshot listener is invoked in your front-end following the local write ("Local writes in your app will invoke snapshot listeners immediately"), this value is not set (item.createdAt.toDate() generates an error).

One solution is to use the metadata.hasPendingWrites property that indicates whether the document has local changes that haven't been written to the backend yet.

For example:

const ChatMessages = useCallback(() => {
 privateMessage
  .doc(chatId)
  .collection('messages')
  .orderBy('createdAt', 'asc')
  .onSnapshot((querySnapshot) => {
    const messageList = [];
    querySnapshot.forEach((doc) => {
      messageList.push(doc.data());
    })
    if (!querySnapshot.metadata.hasPendingWrites) {  // <======
       setMessages(messageList);
    }

  }, error => {
    console.log(error)
  })
 }, [chatId])

Upvotes: 2

Related Questions