1

I'm pretty new to react native and I am currently developing a chat/forum application. Recently, I have been having some trouble trying to create a direct message section for my app. I don't know how to connect my database to my frontend. Here is the issue:

I use a mongodb database that consists of two collections: messages and conversations.

Each conversation has a unique Id and each message has a chatId that corresponds to the conversation that it belongs to.

In my react native app, inside a Direct Message component, I have a flatlist that displays the different chats.

When the Direct Message component willMount() I call an async function, getChats(), that fetches the chats from the database that the current user is part of. The fetched chats are then set to the state.

Then, inside getChats(), after the chats are set to the state, I have a for loop that basically loops through the entire this.state.chats array and then calls a function getMessages(this.state.chats[i].id) which fetches all the messages that share the same chatIds as the chats ids. The fetched messages are then added to this.state.messages.

Finally, a flatlist with the props,

keyExtractor={(item)=>item._id} data ={this.state.chats} renderItem={({item})=>this._renderChats(item)} extraData = {this.state}

,renders the chats.

I want to be able to show the latest messages content and sender in the chat View, however, an error saying that the messages content is undefined.

I think this may be due to the fact that messages aren't set to the state before the chats are rendered, but I'm not sure.

How would you solve this? Would you change the frontend or the backend? Would you change both? Should I share my code to make it easier to understand the problem?

Thanks in advance.

  • You might want to look into this question: https://stackoverflow.com/questions/27192621/reactjs-async-rendering-of-components. – blaz May 06 '18 at 15:14

1 Answers1

2

Very clear explanation of the problem! In short:

How to build a chat page with conversations overview (showing last message for each conversation) - using React Native, MongoDB, NodeJS and Express


Some notes:

  • use consistent naming, ie. rename chatId to conversationId and chats to conversations

  • try to minimize the internet requests - because they're resource intensive and slow

    • right now, your algorithm makes conversations.count+1 requests each time the page is opened, could be just 1

    • for the first page, you only need the last message in each conversation

      • load the rest of the messages for a conversation when its page is opened
      • thereby, you don't need the extraData field
      • (though caching, see additional notes)
    • eg. using GraphQL query('currentUser.conversations { text, messages(limit 1), ... }')

    • eg. using rest + mongoose:

          // controller
          // see https://stackoverflow.com/questions/32207457/node-js-mongoose-populate-limit
          // populate the first item in conversation.messages
          const conversations = ()=> db.Conversations
            .find(...)
            .populate({ path: 'messages', options: { limit: 1, sort: { created: -1} }}))
            .exec(...)
      
          // router
          router.get('/conversations', ({user}, res)=>
            getConversations({user})
              .then(data=> res.send({data}))
              .catch(error=> res.send({error}))
          )
      
    • (this assumes messages are a virtual property on conversations)

          // schema
          const messageSchema = new Schema({
            text: String, ...
            conversationId: {type: Schema.ObjectId, ref: 'conversation'}
          })
      
          const conversationSchema = new Schema({
            participants: [{type: Schema.ObjectId, ref: 'user'}]
          })
          // add a "virtual" field to conversation called messages
          // https://stackoverflow.com/questions/43882577/mongoosejs-virtual-populate
          conversationSchema.virtual('messages', {
            ref: 'message',
            localField: '_id',
            foreignField: 'conversationId'
          })
          // make sure the "virtual" fields are included in conversion
          conversationSchema.set('toObject', { virtuals: true });
          conversationSchema.set('toJSON', { virtuals: true });
      
  • assume that all data is broken; eg. the app should not crash if the message data is missing. Check that the property exists before accessing it, and convert it into the expected data type before proceeding.

    • Make sure the errors aren't "swallowed"; eg. make sure to always have a .catch and log the error if you have a .then, and paste the error messages to the question, if any.
    • Uncaught TypeError: Cannot read property 'c' of undefined, when doing a.b.c, where you know that a is an object, can be avoided by first checking; if (a.b) use(a.b.c), or with shortcircuit: a.b && use(a.b.c)
  • correct hypothesis, what happend was:

    • ConversationsPage initialized, state.conversations is Empty
    • willMount called -> fetchAllConversations started, takes time, async
    • willMount ends -> render called -> empty list rendered
    • fetchAllConversations' first request finishes -> setState{conversations} -> render called again -> full list rendered (in your case, it crashed because the last message field was missing)
    • fetchAllConversations invokes fetchMessagesForConversations, which makes many api requests, and possibly calls setState multiple times (ineffective), which in turn causes re-render
  • don't forget the loading state

        state = {loading: false}
        render () {
          const {loading} = this.state
          return <FlatList renderHeader={()=> loading? <ActivityIndicator .../>: null} .../>
        }
    
  • instead, a simple fix would be to call setState after all messages have been loaded:

      async fetchAllConversations () {
        const conversations = await api.conversations()
        await Promise.all(conversations.map(c=> c.messages = await api.messagesForConversationId(c._id)))
        // similar to for (let i=0; i<conversations.length; i++) {
        //  const c = conversations[i]; c.messages = await api.messagesForConversationId(c._id)}
        return conversations
      }
      async reload () {
        this.setState({loading: true})
        this.fetchAllConversations()
          .then(conversations=> this.setState({conversations}))
          .catch(showError)
          // stop loading both on error and success
          .then(()=> this.setState({loading: false}))
      }
      state = {loading: false, conversations: []}
      willMount () { this.reload() }
    
  • though a better solution would be to replace fetchAllConversations from above, assuming the use of virual property and population mentioned above server-side:

      async fetchAllConversationsIncludingLastMessage () {
        const conversations = await api.conversationsWithLastMessage()
        return conversations
      }
    
  • this would reduce the flow to:

    • ConversationsPage initialized, state.conversations is Empty
    • willMount called -> reload started, takes time, async
    • willMount ends -> render called -> loading indicator rendered
    • reload's only request finishes -> setState{conversations} -> render called again -> full list rendered

Additional notes:

  • look into Docker for simplified server setup (ie. to get MongoDB + Node.js running together)
  • I presume you've got a middleware that does the authentication, + correct authorisation logic in the query (eg. only find the conversations/messages the authorised user should have access to)
    • ie. db.Conversation.find({participants: {$includes: req.user._id}}) // pseudocode
    • ie. in messages, first see if the conversation with that id has the user as participant
  • how do you handle pagination? (eg. to prevent slow data fetching and slow UI when there are many posts) (tips: use a "cursor" instead of "offset" - prevents duplication issue etc)
  • Use some library to cache the data locally to improve perceived and actual loading time.
    • Central state management (using eg. Mobx, Redux, Apollo...) solves some of that
    • if you're going to use REST anyhow, make an API wrapper helper + look into mobx
    • otherwise, check out GraphQL and Apollo or similar
halfer
  • 19,824
  • 17
  • 99
  • 186
Leonard Pauli
  • 2,662
  • 1
  • 23
  • 23