reactjssocket.iouse-effectchangestream

Apply new array changes to useEffect with socket.io and change streams


I'm currently building out a social platform, I making use of Mongo Change Streams to detect any changes, where the changes will be "emit" to separate sockets, the previous issue I've had was, that everything had worked perfectly fine, but now due to implementation of nested comments and replies my database structure has changed. So I've come up with a work around by merging the three separate arrays into a single array with the nested arrays information associated with likes and comments. So what I'd like to have is that I'd like to grab the data from the separate sockets (posts, likes, comments) and merge into one, thus where I've already applied an handler to check for any changes between the old and new data, so basically I'm only stuck on merging these three sockets data into one array, I'm abit lost on how to take this approach.

change streams

const db = mongoose.connection;
db.once('open', () => {
    //post changes
    console.log(chalk.blueBright("Setting change streams"));
    db.collection('posts').watch()
    .on('change', change => {
        if(change.operationType === 'insert'){
            console.log(chalk.yellowBright('INSERTED'))
            io.emit('posts', change.fullDocument);
            console.log(change.fullDocument)
        }
    });
    db.collection('posts').watch({ fullDocument: 'updateLookup' })
    .on('change', change => {
        if(change.operationType === 'update'){
            console.log(chalk.yellowBright('UPDATED'));
            io.emit('posts', change.fullDocument);
            console.log(change.fullDocument)
        }
    })

    //comments changes
    db.collection('comments').watch({ fullDocument: 'updateLookup' })
    .on('change', change => {
        if(change.operationType === 'insert'){
            console.log(chalk.yellowBright('INSERTED'))
            io.emit('comments', change.fullDocument);
            console.log(change.fullDocument)
        }
    });
    db.collection('comments').watch({ fullDocument: 'updateLookup' })
    .on('change', change => {
        if(change.operationType === 'update'){
            console.log(chalk.yellowBright('UPDATED'));
            io.emit('comments', change.fullDocument);
            console.log(change.fullDocument)
        }
    })
}); 

client-side

useEffect for detecting any changes posts

//Check socket changes
    useEffect(() => {
      //merged posts with comments and likes sockets?
      //this is the solution I've come up on merging three arrays but
      //I'd like to apply this with the three sockets data that are being retrieved or applied
      const newPosts = posts.map(post => ({
        ...post,
        likes: likes.filter(like => post.likes.map(like => like._id).includes(like._id)),
        comments: comments.filter(comment => post.comments.map(comment => comment._id).includes(comment._id)),
      }))

      const handler = (item) => {
        setPosts((oldPosts) => {
          const findItem = oldPosts.find((post) => post._id === item._id);
          if (findItem) {
            return oldPosts.map((post) => (post._id === item._id ? item : post));
          } else {
            return [item, ...oldPosts];
          }
        });   
      };

      //Sockets
      //socket.off("comments", handler);
      //socket.off("likes", handler);

      return () => socket.off("posts", handler);
       //eslint-disable-next-line react-hooks/exhaustive-deps
    }, []);

Solution

  • There has been two solutions to this, first one is changing how the socket is being called to socket.onMany(handler) which adds a listener that will be fired when any event is emitted. there, the output would be stored as [["posts",{}],["comment",{}],["likes"],{}], the next thing would do is to remove the string values with the use of ".splice(indexOf('posts'))", then you would need to flatten the three arrays into objects, and to merge them with one another by the associated id's likes:

    const newPosts = posts.map(post => ({
        ...post,
        likes: likes.filter(like => post.likes.map(like => like.id).includes(like.id)),
        comments: comments.filter(comment => post.comments.map(comment => comment.id).includes(comment.id)),
    }))
    

    The second approach would be to modify the change streams, to call the Post Model and to make use of populate(), which will every time call a new request to the database with the populated data as needed and then emitting it to your socket, therefor since posts would be the parent document, only changes should be checked in the posts socket.

    //post changes
        console.log(chalk.blueBright("Setting change streams"));
        db.collection('posts').watch()
        .on('change', change => {
            if(change.operationType === 'insert'){
                PostModel.find({}).lean().populate('comments')
                .then((posts) => io.emit('posts', posts))
            }
        })
        db.collection('posts').watch({ fullDocument: 'updateLookup' })
        .on('change', change => {
            if(change.operationType === 'update'){
                PostModel.find({}).lean().populate('comments')
                .then((posts) => io.emit('posts', posts))
            }
        })