Search code examples
javascriptreactjssocket.iouse-effectuse-state

How to receive SocketIO events in React while using states


This is part of my code, what I want to do is this component at any time can receive a message on any of the conversations. Sending a message triggers a Socket event which triggers this code below, but I can't seem to get the "latest" conversations, as the useEffect only triggers when the component mounts (at that point my conversations array has zero length).

What I was thinking is that I should include "conversations" on the useEffect's dependency but that would create multiple websocket connection, one each time a Socket.io event is triggered because it does change the state. Is this the best solution? Thanks in advance!

const [conversations, setConversations] = useState<Array<Conversations>>([]);

useEffect(() => {
    async function getConversations() {
      try {
        const { data } = await axios.get("/api/conversations/");

        if (data.success) {
          setConversations(data.details);
        }
      } catch (err) {}
    }

    getConversations();

    socketInstance.on("connect", () => {
      console.log("Connecting to Sockets...");
      socketInstance.emit("authenticate", Cookies.get("token") || "");
    });

    socketInstance.on("ackAuth", ({ success }) => {
      console.log(
        success
          ? "Successfully connected to Sockets"
          : "There has been an error connecting to Sockets"
      );
    });

    socketInstance.on("newMessage", (data) => {
        const modifiedConversation: Conversations = conversations.find(
      (conv: Conversations) => {
        return conv.conversationId === data.conversationId;
      }
    );

    modifiedConversation.messages.push({
      from: {
        firstName: data.firstName,
        lastName: data.lastName,
        profilePhoto: data.profilePhoto,
        userId: data.userId,
      },
      content: data.content,
      timeStamp: data.timeStamp,
    });

    const updatedConversations = [
      ...conversations.filter(
        (conv) => conv.conversationId !== data.conversationId
      ),
      modifiedConversation,
    ];

    setConversations(updatedConversations);
    });
  }, []);

Solution

  • While attaching and removing the socket listeners every time conversations changes is a possibility, a better option would be to use the callback form of the setters. The only time you reference the state, you proceed to update the state, luckily. You can change

    socketInstance.on("newMessage", (data) => {
      const modifiedConversation: Conversations = conversations.find(
      // lots of code
      setConversations(updatedConversations);
    

    to

    socketInstance.on("newMessage", (data) => {
      setConversations(conversations => {
        const modifiedConversation: Conversations = conversations.find(
        // lots of code
        setConversations(updatedConversations);
    

    You should also not mutate the state, since this is React. Instead of

    modifiedConversation.messages.push({
    

    do

    const modifiedConversationWithNewMessage = {
      ...modifiedConversation,
      messages: [
        ...modifiedConversation.messages,
        {
          from: {
            // rest of the object to add