0

After weeks of debugging and having to implement an awful workaround to get my react app working, I just figured out the issue and as a beginner with react, this just confused me so I'm posting this question to hear your suggestions.

I have a react app or rather Ionic react app (but its really the same as a normal react web app), where I'm using the famous socket.io library to communicate with a backend and receive messages in real time.

For the sake of simplicity, here is how my code is built:

import React, { useEffect, useState } from 'react';
import socketIOClient from 'socket.io-client';
// bunch of other imports ....

const serverHost = config.localUrl;
const socket = socketIOClient(serverHost);

const App: React.FC = () => {

 
  const [state1, setState1] = useState([]);
  const [state2, setState2] = useState([]);

  useEffect(() => {
    socket.on('connect_error', () => {
      console.log("connection error .. please make sure the server is running");
      // socket.close();
    });

    return () => {
      console.log("deconnecting the socket... ");
      socket.close();
    }
  }, [])

  useEffect( () => {
    socket.emit('init', "initialize me");
    socket.on('onInit', (configs: any) => {
         setState1(configs);
    });
  }, [])


  const reset = () => {
    socket.removeAllListeners(); // the focus is on this line here.
    state1.forEach( (s: any) => {
      s.checked = false;
      s.realTimeValue = "";
    })
    setState1([]);
  }

  
  return (
    <IonApp>
      <IonToolbar color="primary">
        <IonTitle >Test</IonTitle>
      </IonToolbar>
      <IonContent>
      
      
        <Component1
          socket={socket}
          reset={reset}
        />

        <IonList>
          {state1.map((s: any, idx: number) =>
            <Component2 key={s.name}
              s={s}
              socket={socket}
              
            />)
          }

          
        </IonList>
      </IonContent>

      <CustomComponent socket={socket} />

    </IonApp>
  );
};

export default App;

As you can see, my app is simple. I'm passing the socket object in order to listen on events in the child component, which works fine until one day I noticed that if the user deleted one of the Component2 in the UI, then I would have a warning that socket.io received an event but the component already unmounted and it will cause memory leak. It's a famous warning in react, here is the warning:

Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and clean up listeners.

After googling, I found that socket.io have a built in function to do this, which is the socket.removeAllListeners() I'm calling in the reset function. Here it become interesting, this worked fine, now the user can delete safely. However, the socket.on call in the CustomComponent (the last component in the app) is not working anymore. If I comment the socket.removeAllListeners() line in the reset function, then the socket.on call in the CustomComponent start listening and receiving message again.

Surprisingly, this does not work only with the last component in my app, which is the CustomComponent. However, it works fine for the other components! As you can see in the code, I'm passing the reset function as a props to the Component1, so it have nothing to do with the CustomComponent.

Someone have an idea why this doesn't work and how to solve it?

Note

The workaround I implemented was to move the socket.on function in the CustomComponent inside a useEffect so that it will always be triggered when ComponentDidMount and ComponentDidUpdate happens. The catch here is that the socket.on fires more than one time. So if I receive a message from server then I see in the browser that the function get called 5 times in a row.

This and this questions are also related to my question here.

trixn
  • 15,761
  • 2
  • 38
  • 55
basilisk
  • 1,156
  • 1
  • 14
  • 34

1 Answers1

1

socket.removeAllListeners() will remove all listeners from the socket including listeners that have been added by components that are still mounted and listening. A component should call socket.on when it mounts and socket.off when it unmounts. This can be achieved by using useEffect:

const [configs, setConfigs] useState([]);

useEffect(() => {
    const onInit = configs => setConfigs(configs);

    socket.on('onInit', onInit);

    socket.emit('init', "initialize me");

    // return a function that unsubscribes the handler from the socket
    // you have to pass the handler which you passed to socket.on before to only remove that handler
    return () => socket.off('onInit', onInit);
}, []);

the rule of thumb is that the component that subscribes to something as a side-effect of mounting it also has to unsubscribe when it unmounts. it should never do just one of both and it should only unsubscribe from what it subscribed to itself. Calling socket.removeAllListeners() when the component only subscribed to a specific event is very error prone. It will break any other components subscriptions.

A component shouldn't close a socket if it didn't open it and it should not subscribe to a signal that is doesn't also unsubscribe from. Not keeping your side effects that belong together at one place will give you a lot of headaches.

trixn
  • 15,761
  • 2
  • 38
  • 55
  • Thanks for the hints! I can do that with the init event since it is inside useEffect, however, how about when I want to call socket.emit if the user click a button for example. Then I need to declare the event when the user click the button and hence how can I clean it if it's not in the useEffect in this case? – basilisk Oct 01 '20 at 12:49
  • @basilisk `.emit()` doesn't attach a listener to anything so I don't see what you would need to clean up. Or maybe I don't understand the question. – trixn Oct 01 '20 at 13:00
  • What I meant is that my listener function that I pass to socket.on depends on other variables which I will not receive in the payload from the server. So how I will move it inside the useEffect in this case? Furthermore, I tried to move my listener function to the useEffect but the compiler is yelling at me to put the props.socket in the array dependency. Should I do this? – basilisk Oct 01 '20 at 19:50
  • Also I didn't find this socket.off function in the official docs of socket.io! Should I use it or did they changed it somehow? anyway thank you very much, your hints are very helpful. I will accept your answer ;) – basilisk Oct 02 '20 at 10:45
  • @basilisk It's kind of hidden in the docs but below [`socket.on`](https://socket.io/docs/client-api/#socket-on-eventName-callback) it says: "The socket actually inherits every method of the [Emitter](https://github.com/component/emitter) class, like `hasListeners`, `once` or `off` (to remove an event listener)". You should definitely use it to remove only the handlers that you actually added in a component. Everything else would introduce side effects that get hard to handle. Glad that my answer helped you. – trixn Oct 02 '20 at 10:50