Search code examples
javascriptreactjssocketseventschat

Update component on socket.io event - ReactJS, Socket.io


I have a React chat app working with Socket.io. I can send messages from different sources but can't update every source that is connected to the socket. I tried to do it thought componentDidUpdate, using refs and e.t.c to keep the socket, but nothing worked.

Socket:

const io = require('socket.io').listen(3002);
const jwt = require('jsonwebtoken');

const messages = [];

io.use((socket, next) => {
  if (socket.handshake.query && socket.handshake.query.token) {
    jwt.verify(
      socket.handshake.query.token,
      '4-8-15-16-23-42',
      (err, decoded) => {
        if (err) return next(new Error('Authentication error'));
        socket.decoded = decoded;
        next();
      }
    );
  } else {
    next(new Error('Authentication error'));
  }
}).on('connection', socket => {
  socket.emit('connected', {
    user: socket.decoded.username
  });

  socket.on('newMessage', message => {
    messages.push(message);
    socket.emit('messages', messages);
  });
});

Client:

import { useState, useEffect } from 'react';
import io from 'socket.io-client';

const token = sessionStorage.getItem('token');
const socket = io('http://localhost:3002', {
  query: { token }
});

const Chat = () => {
  const [user, setUser] = useState('');
  const [messages, setMessages] = useState([]);
  const [input, setInput] = useState('');

  useEffect(() => {
    socket.on('connected', msg => {
      setUser(msg.user);
    });
  }, []);

  const inputChangeHandler = e => setInput(e.target.value);

  const sendMessage = () => {
    socket.emit('newMessage', `${user}: ${input}`);
    socket.on('messages', newMessages => setMessages(newMessages));
    setInput('');
  };

  return (
    <div>
      <div id="message-container">
        <div>Greetings {user}!!!</div>
        {messages.map((message, index) => (
          <div key={index}>{message}</div>
        ))}
      </div>
      <input value={input} onChange={e => inputChangeHandler(e)} />
      <button onClick={sendMessage}>Send</button>
    </div>
  );
};

export default Chat;

Solution

  • You should connect to socket when component is mounted, and then attach message handler.
    But the problem is that if you do it in useEffect, your handler want see state changes (your message array will be always empty as in initial state. That's because useEffect has empty array as second argument. It's something like chicken-egg problem. You can't use normal useState or useReducer so...

    Here is solution:

    First remove this:

        socket.on('messages', newMessages => setMessages(newMessages));
    

    from sendMessage function.

    We need some additional package.

    yarn add immer use-immer
    
    import React, {useEffect, useRef } from "react";
    import io from 'socket.io-client';
    import { useImmer } from "use-immer";
    
    const Chat = () => {
    
      const [messages, setMessages] = useImmer({});
    
      useEffect(() => {
    
        const serverAddress = "https://my-chat-server";
        const socket = io( serverAddress,{
          transports: ['websocket']
        });
    
        socket.on("connect", () => {
          console.log("Connected");
          socketRef.current = socket;
        });
    
        socket.on("message", msg => {
    
          setMessages(msgs => {
    
            return msgs.concat(msg);
    
          });
    
        });
    
    
      },[setMessages]);
    
       return <div>Here you know what to do :)</div>
    }