Search code examples
javascriptreactjsgun

map() shows new messages from state only if I type something into input


Every time I send a message, map() only shows my new message when I type something in the input (re-render).

The listener works fine, it displays new messages in the console immediately after sending a new message, curiously, the messages state also updates when I look in React Developers Tools,

But useEffect[messages] does not trigger on a new message, and it only displays new messages after I type something in the input.

Here is my component with comments:

import { useState, useEffect, useRef } from "react";

const ChatWindowChannel = ({ window, chat }) => {
  const [messages, setMessages] = useState([]);
  const [message, setMessage] = useState("");
  const loaded = useRef(null);
  const messagesEndRef = useRef(null);

  const channelMessagesHandler = async () => {
    const channelMessagesListener = await chat.loadMessagesOfChannel(window);

    channelMessagesListener.on((msgs) => {

      console.log(msgs); // Here shows new messages after I click `send`
      // Works correct.

      setMessages(msgs); // In React Developers Tools the state `messages` are update if I click `send`
      // Works correct.

      console.log(messages); // Shows always empty array[]
      // Dont works correct 
    });
  };

  async function send() {
    await chat.sendMessageToChannel(window, message, {
      action: "join",
      alias: chat.gun.user().alias,
      pubKey: chat.gun.user().is.pub,
      name: "grizzly.crypto",
    });

    setMessage("");
  }

    useEffect(() => {
      // Shows only once. It shold every time on new message
      console.log("Messages changed");
    }, [messages]);

  useEffect(() => {
    loaded.current !== window.key && channelMessagesHandler();
    loaded.current = window.key;
  }, [window]);

  return (
    <div>
      <div className="ChatWindow">
        <h2>
          Channel {window.name} - {window.isPrivate ? "Private" : "Public"}
        </h2>

        <div>
          <details style={{ float: "right" }}>
            <summary>Actions</summary>
            <button
              onClick={async () => {
                await chat.leaveChannel(window);
              }}
            >
              Leave channel
            </button>
          </details>
        </div>

        <div className="msgs">
          {messages.map((message, key) => (
            <div key={`${key}-messages`}>
              <small>{message.time}</small>{" "}
              <small>{message.peerInfo.alias}</small> <p>{message.msg}</p>
              <div ref={messagesEndRef} />
            </div>
          ))}
        </div>

        <div>
          <div>
            <input
              type="text"
              value={message}
              onChange={(e) => setMessage(e.target.value)}
              onKeyDown={(e) => {
                if (e.key === "Enter") send();
              }}
            />
          </div>
          <div>
            <button onClick={send}>Send</button>
          </div>
        </div>
      </div>
    </div>
  );
};

export default ChatWindowChannel;

Solution

  • Note 1: setMessages(msgs); do not change the value of messages immediately, it will be set only on next render so console.log(messages) just after setting will give you incorrect results.

    Note 2: In case the reference of the array msgs that you are trying to set to a state variable is not changed even if array is modified - re-render will not be executed and useEffects will not be triggered, reference to an array needs to be new.

    setMessages([...msgs]);
    

    Same happens also with {objects}.