[SOLVED] Why state variables got reset on Moralis subscription in React

Hi, I am using React JS (with chakra-ui and @chatscope/chat-ui-kit-react) with Moralis 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.

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?

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

Thanks a lot for any help.

Hey @brianivander,

Based on your code, seems like coz it might take more time for getMessages might take more time due to more lines of codes, therefore setTextMsgs might be executed much later after subscribeMessages has finished executing.

Keep in mind that JS is asynchronous, so getMessages and subscribeMessages will be executed at the same time during first load in useEffect.

Hi @YosephKS thanks for your reply. Actually it’s not the case.

Because as you can see on the render, the getMessages() is already called and I have gotten the textMsgs array to show in the DOM.

The subscribeMessages is initialized at the beginning, but the subscription.on is only trigerred after I created an object in messages’ table in Moralis.

I think there’s something else going here that’s causing this.

ohhh I see :thinking: can you try to use useCallback for subscribeMessages and add textMsgs as a dependency, I am afraid this might be the case where textMsgs will not update because it’s defined in useEffect

Nothing changes.

This is the latest code

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, useCallback } from "react";

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

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

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

    // on subscription object created
    subscription.on("create", (object) => {
      updateMsgs();
    });
  }, [textMsgs]);

  const updateMsgs = () => {
    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>
  );
};

And this is the screenshot

I’m pretty user it’s because the useState is being called again and reset to it’s initial state. Because when I change the
const [textMsgs, setTextMsgs] = useState([]);

Now whenever the subscription.on is called, it’s now returning an empty array instead of an empty string.

hmmm what a strange, but now I am just guessing could it be because you don’t have the textMsgs as the dependency so it doesn’t update the subscribeMessages in useEffect. If still not work, I think we can try to use the useSubscription hooks here https://github.com/YosephKS/react-moralis#usemoralissubscription

Now I’ve added textMsgs as dependency in useEffect

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, useCallback } from "react";

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

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

  const subscribeMessages = useCallback(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);
    });
  }, [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>
  );
};

Few things happening:

  1. The first time console.log in subscription.on is called, it’s still an empty array.
  2. About 1-2 second later, the console.log inside subscription.on is being called 4 times.

There is no useSubscription. Do you mean useMoralisSubscription()?

1 Like

niceeee, oh yes it is, it’s the same with subscription.on under the hood, but we already made it into hook form

Actually I’m not satisfied with the result here. Because the way it happen here is not that I’m using the subscription.on, but I fetch the data again from getMessages.

It also takes more than 1 second after I click send, then the data is received. Imagine having hundresds of chats, it will take more time to render after I click send. It’s not a good user experience. Need to find another way. I might look into useMoralisSubscription.

1 Like

I think you just need to separate the getMessages and subscribeMessages in different useEffect coz yeah since they are in the same one, it will refetch the messages again and again and yes I get what you mean it’s not good for the UX.

Something like this might prevent the refetching so getMessages just called once in the beginning.

useEffect(() => {
    getMessages();
}. []);

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

useMoralisSubscription is also a good solution, at least using that it’ll abstract away the complexities you that you have here when implementing subscription.on.

Separating the useEffect makes the result of console.log now undefined.

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, useCallback } from "react";

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

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

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

  const subscribeMessages = async (textMsgs) => {
    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>
  );
};


This is frustrating. I’ll try useMoralis Subscription.

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.