0

I'm building a chat application in react. Now the problem that I ran into, is when I click on a button, the handleMsgSend button runs, and dispatches a 'newMsg' event to the other client. On receiving this event, I expect the msgArr state to update with the newly received msg, but no. All in it is the new msg, and nothing else. Why is that happenning? I feel like I skipped technical stuffs while learning react.

export default function Chat (props) {
    const uuid = props.uuid;
    const classes = useStyles();
    const [open, setOpen] = useState(false);
    const [activeStatus, setActiveStatus] = useState('Offline');
    const [unreadMsgs, setUnreadMsgs] = useState(0);
    const [msgArr, setMsgArr] = useState([]);
    const chatBtnRef = useRef();
    const chatBoxRef = useRef();
    const msgInputRef = useRef();
    useEffect(() => {
        socket.emit('join', uuid);
        socket.emit('isOnline', uuid);
        socket.on('newMsg', msg => {
            setMsgArr([ ...msgArr, { type: 'received', msg }]);
            console.log(msgArr);
            if(!open) setUnreadMsgs(unreadMsgs + 1);
            chatBoxRef.current.scrollTop = chatBoxRef.current.scrollHeight;
        });
        socket.on('isOnline', () => {
            setActiveStatus('Online');
            socket.emit('isOnline');
        });
        return () => {
            console.log('removed');
            socket.off('newMsg');
            socket.off('Online');
        }
    }, []);

    const handleMsgSend = e => {
        e.preventDefault();
        let msg = msgInputRef.current.value;
        setMsgArr([ ...msgArr, { type: 'sent', msg }]);
        e.currentTarget.reset();
        socket.emit('newMsg', {uuid,  msg});
        chatBoxRef.current.scrollTop = chatBoxRef.current.scrollHeight;
    }
    const toggleChat = () => {
        setUnreadMsgs(0);
        if(open) setOpen(false);
        else setOpen(true);
    }

How I am rendering the msgArr:

                <div ref={ chatBoxRef } className={ classes.chatBox }>
                    {
                        msgArr.map((msgObj, index) => {
                            return (
                                <div key={index} className={`msg-container ${(msgObj.type == 'sent')? 'myMsg' : 'hisMsg'}`}>
                                    <span className='msg'>
                                        { msgObj.msg }
                                    </span>
                                </div>
                            )
                        })
                    }                     
                </div>
  • If you're relying on `console.log(msgArr);` right below `setMsgArr([ ...msgArr...` to tell you what's in the state array, then it will give you wrong info, because `setMsgArr([ ...msgArr, { type: 'received', msg }]);` is an asynchronous call and you shouldn't expect the state variable to change immediately. – codemonkey Mar 09 '21 at 17:22
  • As @codemonkey already pointed out, state updates don't reflect immediately as they are async and affected by closures, Please check [this post](https://stackoverflow.com/questions/54069253/usestate-set-method-not-reflecting-change-immediately/54069332#54069332]) for more details – Shubham Khatri Mar 09 '21 at 17:25
  • okay, but at least it should show a couple of previous msgs, but it doesn't. It seems as if the whole msgArr is reset after ever 'newMsg' trigger –  Mar 09 '21 at 17:25
  • @ShubhamKhatri the problem is killing me literally. –  Mar 09 '21 at 17:27
  • @nikeshlepz how are you using `msgArr` for rendering – Shubham Khatri Mar 09 '21 at 17:30
  • @ShubhamKhatri I've added it in the edit. –  Mar 09 '21 at 17:34

1 Answers1

1

your socket listener is attached inside a useEffect which is executed only on initial render, so the value of msgArr which you see inside it will always refer to the initial value defined in state due to closure.

Now since msgArr is used from closure in socket listener, you will always be refering to the initial state and not the updated one even when you receive new messages.

To solve this, you must use callback approach to update state

   socket.on('newMsg', msg => {
        setMsgArr(prevMsgArr => [ ...prevMsgArr, { type: 'received', msg }]);
        if(!open) setUnreadMsgs(unreadMsgs + 1);
        chatBoxRef.current.scrollTop = chatBoxRef.current.scrollHeight;
    }); 

Once you do that you would be able to see the updated value of state in the next render cycle and consequently be able to render the updated content based on the array

P.S. Please note that console.log(msgArr); immediately after setMsgArr will not give you the updated value since state updates are async and affected by closure. Refer this post for more insight on this.

Shubham Khatri
  • 270,417
  • 55
  • 406
  • 400
  • Very interesting. Always thought you only need that callback when you actually mutate the state variable before setting it. It's frankly a bit surprising to me that `setMsgArr([ ...msgArr, { type: 'received', msg }]);` will keep reusing the same variable to be honest. I mean, wouldn't it always reference `const [msgArr, setMsgArr] = useState([]);` which does get updated? Anyway, the callback works properly. – codemonkey Mar 09 '21 at 19:25
  • Okay I'm convinced the msgArr of the initial render was being used. "...you will always be refering to the initial state and not the updated one even when you receive new messages....", I was able to receive new msg, it was just that all previous msgs were lost. And after a little time experimenting, I've found out that, on a component rerender, all states of the preceding renders will reset to the value they were initialized to. So on firing 'newMsg' event, only a single msg was being appended into the empty msgArr. It's quite peculiar behaviour tbh. Honestly, I have to look more into it. –  Mar 10 '21 at 01:24
  • @nikeshlepz the callback approach I mentioned in my answer is thee way to go about such situations since callback provided you with the latest state – Shubham Khatri Mar 10 '21 at 05:26
  • @codemonkey the the problem is that the when the useEffect function gets called, its takes the values from its enclosing closure and never updates it until called again which is why you always refer to the initial state. – Shubham Khatri Mar 10 '21 at 05:28
  • 1
    Makes perfect sense. I have tested it in a Sandbox by doing an `addEventListener` inside `useEffect`. And this behavior persists until you change to a callback. So as far as I am concerned, your answer perfectly explains the OP's problem and offers a great solution. – codemonkey Mar 10 '21 at 05:33