1

Problem

Hi everyone, I am trying to create a realtime messaging application. I used useState and useEffect to keep track of when I want components to re-render. However, the problem I am running into is infinite loops inside my useEffect. I'm wondering if someone can push me in the right direction on how to solve this issue without using setState inside useEffect so I can avoid infinite loops.

Things I have tried

Client Code

// Keeps track of whether message was successfully sent
const [isMessageSent, setIsMessageSent] = useState({
    status: "failure",
    render: false
});

useEffect(() => {    
    // Connecting to our endpoint and creating an event
    socket = io(ENDPOINT, connectionOptions);

    // If message has been successfully sent...
    if (isMessageSent.status === "success") {
        socket.emit("messageToServer", isMessageSent);
    }

    // Now that the server has acknowledged message has beent sent
    // we will set it back to false and reredner everything
    socket.on("messageFromServer", ({ status, render }) => {
        // Triggering a re-render since message was successful
        setIsMessageSent({ ...isMessageSent, status, render });
        // Setting it back to default in the event of a new message
        setIsMessageSent({ ...isMessageSent, status: "failure", render: false });
    });
}, [history, ENDPOINT, isMessageSent]);

Server Code

// Socket.io related code
io.on("connection", (socket) => {
    socket.on("messageToServer", (isMessageSent) => {
        // That means message was successfully sent, set it back to false
        if (isMessageSent.status === "success") {
            io.emit("messageFromServer", { ...isMessageSent, render: true });
        }
    });
});
Andy
  • 303
  • 1
  • 9
  • 1
    So under what circumstances *do* you want your `useEffect` to run all that code aside from the first render? Do you actually want it to run when a change to `isMessageSent` or the other 2 variables is made? – codemonkey Mar 01 '21 at 06:44
  • I want my useEffect to run when a change is made to isMessageSent, that's my queue to re-render every component for all connected users. – Andy Mar 01 '21 at 06:48
  • 1
    If `isMessageSent` is passed in the second argument to `useEffect` and `setIsMessageSent` is called within the `useEffect` hook, you're creating an infinite loop, because the state change causes a re-render, which triggers useEffect because the value of `isMessageSent` has changed. It's like saying, "when the value of x changes, change the value of x". – Mike Mar 01 '21 at 06:49
  • One hack could be to stringify your object before you store it in a state variable. That way when useEffect updates it, it won't trigger a re-render because the string value has not changed. If you update it with an object, useEffect will think it's different every time. – codemonkey Mar 01 '21 at 06:53
  • Hmm, I'm trying to come up w/ a way to work around this problem but it's not like I can pass setIsMessageSent to my back end. – Andy Mar 01 '21 at 06:54
  • Also, you will have to do something about those back-to-back `setIsMessageSent`s. That won't work as state updates are asynchronous. – codemonkey Mar 01 '21 at 06:56
  • I am pretty sure that the setting of your state two times inside `messageFromServer` event should be handled in a loosely coupled way. Don't hack your way through this. It will blow up later on. Take a pen and paper and try to draw the flow to really understand how you can decouple this. – Lakshya Thakur Mar 01 '21 at 06:56
  • Are you using [eslint-plugin-react-hooks](https://www.npmjs.com/package/eslint-plugin-react-hooks)? I'm not suggesting you should, just wondering if one of the issues you're experiencing is trying to get it to work with this. – Patrick Roberts Mar 01 '21 at 06:56
  • @codemonkey state updates aren't always asynchronous. The problem is that each render creates a new closure for captured references like `isMessageSent`. – Patrick Roberts Mar 01 '21 at 06:57
  • 1
    Thanks for your feedback everyone! I think the way I wrote this is pretty poor and I'm going to go back to the drawing board and work on a different implementation – Andy Mar 01 '21 at 06:59
  • @Andy you don't have to close or delete your question, people are trying to get a better understanding of what you're trying to accomplish before attempting to answer your question. – Patrick Roberts Mar 01 '21 at 07:00
  • @Andy When re-architecting this, one think I can suggest is to create a socket connection as well as registering all the listeners **one-time** like in an empty dependency `useEffect`. Later on for any state related changes , you can always create more `useEffects` that only trigger when that particular state value changes. Also for any state update that depends on the previous one, you can use the callback syntax inside your **state updator** function since this might help you dodge the usage of more dependency in your `useEffect` deps array. – Lakshya Thakur Mar 01 '21 at 07:04
  • @Patrick Ah, okay. Based off of what everyone say, it made me realize this wasn't the best way to go about it haha. My goal is to trigger a re-render for every client connected as soon as a user sends a message. That means my database has changed, and everyone needs to be retrieving information from the same database in that point in time. – Andy Mar 01 '21 at 07:06
  • @Lakshya Thanks! I really like that approach. It hadn't occurred to me that I could add the listeners into my dependency array. I'm going to look into this approach more! – Andy Mar 01 '21 at 07:12
  • 1
    @Andy in that case, consider the fact that socket.io is already a push-based system. The server is capable of broadcasting messages to every client, so you should architect an event for users to subscribe to the relevant database change and use that client-side callback to set your component state. Right now it looks like you're trying to use one server-sent message as a signal for the user to emit another message and request information from the server that should have just been sent in the first place. – Patrick Roberts Mar 01 '21 at 07:12
  • @Andy I do not mean to use socket listeners in the dependency array. I mean to simply create a new socket connection and registering all those inside a `useEffect` which has an empty dependency array. Currently your `isMessageSent` state is coupled with socket creation and listener logic. It doesn't have to be. That can use it's separate `useEffect`. – Lakshya Thakur Mar 01 '21 at 07:13
  • One more thing. If you app is going to have routes, bring in a global state using React Context API or Redux to share the socket connection among routes. Your socket connection is a one-time op and should exist as a singleton instance across your entire client side app. I am not aware of a hook based approach so suggested these as of now. – Lakshya Thakur Mar 01 '21 at 07:19
  • 1
    Thanks everyone! Your feedback has gave me a lot to work with. Going to start planning things out now – Andy Mar 01 '21 at 07:22

0 Answers0