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.