Search code examples
javascriptreactjsmoralis

Why state variables got reset on Moralis subscription in React


I am using React JS (with chakra-ui and @chatscope/chat-ui-kit-react) with Moralis Web3 creating a chat app where I want to:

  1. Get all the messages when component is loaded the first time, and
  2. Subscribe to messages whenever there’s a new message added in the table. This is using websocket provided by Moralis.

I am able to get the first step where I call a query when the component is loaded the first time using useEffect. I store it in a state variable textMsgs.

The problem is when I subscribe, in the

subscription.on("create", (object) => {
      console.log(textMsgs);
});

the console.log result is always empty. Whereas it should show the last array of what I fetch initially.

I then add a

<Button onClick={() => console.log(textMsgs)}>get textMsgs</Button>

to check if the textMsgs can show the array, and it DOES show the array.

I’m really confused why in subscription.on, the textMsgs is empty, but when I click the button, why is it showing the first fetched when component is loaded?

Full Code: Messages.js

import { Container, Button, Text } from "@chakra-ui/react";
import Moralis from "moralis/dist/moralis.min.js";
import styles from "@chatscope/chat-ui-kit-styles/dist/default/styles.min.css";

import {
  MainContainer,
  ChatContainer,
  MessageList,
  Message,
  MessageInput,
} from "@chatscope/chat-ui-kit-react";
import { useState, useEffect } from "react";

export const Messages = ({ roomId, userId }) => {
  const [textMessage, setTextMessage] = useState("");
  const [textMsgs, setTextMsgs] = useState("");

  useEffect(() => {
    getMessages();
    subscribeMessages();
  }, []);

  const subscribeMessages = async () => {
    const query = new Moralis.Query("Messages");
    query.equalTo("roomId", roomId);
    const subscription = await query.subscribe();

    // on subscription object created
    subscription.on("create", (object) => {
      console.log(textMsgs);
    });
  };

  // Get textMsgs in this room
  const getMessages = async () => {
    const Message = Moralis.Object.extend("Messages");
    const message = new Moralis.Query(Message);
    message.equalTo("roomId", roomId);
    const msgResults = await message.find();

    var msgs = [];

    for (let i = 0; i < msgResults.length; i++) {
      var username = await getUsername(msgResults[i].attributes.userId);

      var msg = {
        msgId: msgResults[i].id,
        createdAt: msgResults[i].attributes.createdAt,
        userId: msgResults[i].attributes.userId,
        textMessage: msgResults[i].attributes.textMessage,
        username: username,
      };

      msgs.push(msg);
    }

    setTextMsgs(msgs);
  };

  const getUsername = async (userId) => {
    // Query username
    const User = Moralis.Object.extend("User");
    const user = new Moralis.Query(User);
    user.equalTo("objectId", userId);
    const userResults = await user.find();

    return userResults[0].attributes.username;
  };

  const sendMessage = (e) => {
    var newMsg = {
      textMessage: textMessage,
      userId: userId,
      roomId: roomId,
    };

    const Message = Moralis.Object.extend("Messages");
    const message = new Message();

    message.set(newMsg);
    message.save().then(
      (msg) => {
        // Execute any logic that should take place after the object is saved.
        //alert("New object created with objectId: " + msg.id);
      },
      (error) => {
        // Execute any logic that should take place if the save fails.
        // error is a Moralis.Error with an error code and message.
        alert("Failed to create new object, with error code: " + error.message);
      }
    );
  };

  return (
    <Container>
      {/* react chat */}
      <div
        style={{
          height: "100vh",
        }}
      >
        <Button onClick={() => console.log(textMsgs)}>get textMsgs</Button>
        <MainContainer style={{ border: "0" }}>
          <ChatContainer>
            <MessageList>
              <MessageList.Content>
                {textMsgs &&
                  textMsgs.map((data, key) => {
                    return (
                      <div key={"0." + key}>
                        <Text
                          key={"1." + key}
                          align={data.userId === userId ? "right" : "left"}
                          fontSize="xs"
                        >
                          {data.username}
                        </Text>
                        <Message
                          model={{
                            message: data.textMessage,
                            direction:
                              data.userId === userId ? "outgoing" : "incoming",
                            sentTime: "just now",
                            sender: "Joe",
                          }}
                          key={"2." + key}
                        />
                      </div>
                    );
                  })}
              </MessageList.Content>
            </MessageList>
            <MessageInput
              value={textMessage}
              placeholder="Type message here"
              attachButton={false}
              onChange={(text) => {
                setTextMessage(text);
              }}
              onSend={sendMessage}
            />
          </ChatContainer>
        </MainContainer>
      </div>
    </Container>
  );
};

Here you can see the Messages.js:30 (in subscription.on) and Messages.js:102 (in ) showing different result enter image description here


Solution

  • I fixed this. So when we want to update an array state variable, instead of trying to copy the variable and use array.push(), we must use the setState function to get the last state and use spread operator to add the new object in the array.

    Example This is the state variable, the textMsgs is filled with an array of objects, example [‘apple’, ‘banana’, ‘lemon’]

    const [textMsgs, setTextMsgs] = useState([]);
    

    When we want to add another object, instead of doing this first way

    var messages = textMsgs;
    messages.push('mango');
    setTextMsgs(messages);
    

    we should do the second way

    var message = 'mango'
    setTextMsgs(oldarr => [...oldarr, message])
    

    Because in the first way, somehow when I’m fetching the data from textMsgs it’s empty. I’m not sure why it happened, but if you guys have this issue you want to try the second way.