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.
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 callsocket.on
when it mounts andsocket.off
when it unmounts. This can be achieved by using useEffect: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.