Search code examples
reactjsmongodbsocket.ioreact-state-management

How to prevent React from re-rendering every function in the function component


I am having two users have a private message by having both of them join the same Socket.io room, which is based on the room created from the sender and receiver ids. Every time I create a message, the client rejoins the room, due to a Socket.io emit event re-rendering because of a state change, and the roomId is console logged. As a result, the message is not being transmitted over multiple instances of the chat when it has joined the same room.

const ChatApp = () => {
  const [messages, setMessages] = React.useState([]);
  const userId = useSelector(state => state.profile.profile._id);
  const senderFullName = useSelector(state => state.auth.user.fullname);
  const authId = useSelector(state => state.auth.user._id);
  const roomId = [authId, userId].sort().join('-');
  const ioClient = io.connect("http://localhost:5000"); 

    ioClient.emit('join', {roomid: roomId});
    ioClient.emit('connected', {roomid: roomId} );
    ioClient.on('message', (msg) => {
      const messageObject = {
        username: msg.from,
        message : msg.body,
        timestamp: msg.timestamp
      };
      addMessage(messageObject);
    });

  const addMessage = (message) => {
   const messagess = [...messages];
   messagess.push(message); 
   setMessages(messagess);
   console.log(messagess);   
   }

   const sendHandler = (message) => {
    var res = moment().format('MM/DD/YYYY h:mm a').toString();

    // Emit the message to the server
    ioClient.emit('server:message', {from: senderFullName, body: message, timestamp: res });
   }
   
     return (

<div className="landing">
<Container>
 <Row className="mt-5">
   <Col md={{ span: 8, offset: 2 }}>

     <Card style={{ height: "36rem" }} border="dark">
       <Messages msgs={messages} />
       <Card.Footer>
       <ChatInput onSend={sendHandler}>
         </ChatInput>
       </Card.Footer>
     </Card>
   </Col>
 </Row>
</Container>
</div>
   )
 };

Api code that is used in server

    io.sockets.on("connection",function(socket){
        // Everytime a client logs in, display a connected message
        console.log("Server-Client Connected!");

        socket.on('join', function(data) {

           socket.join(data.roomid, () => {
               //this room id is being console logged every time i send a message
               console.log(data.roomid);
           }); 

        });    

         socket.on('connected', function(data) {    
            //load all messages
            console.log('message received');
            (async () => {
                try {
                    console.log('searching for Schema');
            const conversation = await Conversation.find({roomId: data.roomid}).populate('messages').lean().exec();
            const mess = conversation.map(); 
            console.log(mess);   
            console.log('Schema found');    
            }
               catch (error) {
                   console.log('Schema being created');
                Conversation.create({roomId: data.roomid});
               }
        })();

    });

        socket.on('server:message', data => {
 
            socket.emit('message', {from: data.from, body: data.body, timestamp: data.timestamp});
            console.log(data.body);
            Message.create({from: data.from, body: data.body});

                
        })     
    });


Solution

  • You could either move the socket state up to a parent component, or move the event initialization into a custom hook that is shared once across your application, or into a useEffect hook here so it only runs on initial render.

    Probably the simplest is the latter option, you can just wrap those lines of code you only want to be written once up like this:

    useEffect(() => {
      ioClient.emit('join', {roomid: roomId});
        ioClient.emit('connected', {roomid: roomId} );
        ioClient.on('message', (msg) => {
          const messageObject = {
            username: msg.from,
            message : msg.body,
            timestamp: msg.timestamp
          };
          addMessage(messageObject);
        });
    }, []);
    

    The empty array at the end signifies that this will only run on initial render, and not again, as it has no dependencies.


    Update

    You may or may not be having closure or timing issues, but to be on the safe side, replace the line "addMessage(messageObject);`" with:

    setMessages((previousMessages) => [messageObject, ...previousMessages]);
    

    This will do the same thing as the addMessage function, but by passing a callback function to setMessages, you avoid using the state object from outside.