DEV Community

Cover image for Socket event listener fires multiple times in React component
Tue
Tue

Posted on • Updated on

Socket event listener fires multiple times in React component

Here is something I've learned doing my side project that I think it's fundamental but also often overlooked.

Some context

The project simply contains 2 major parts:

  • Front-end: React and Materials UI
  • Back-end: REST API using Express, Typescript, Firebase and Socket.io

My goal was to inform the front-end when data is updated using Socket.io so that it would try to fetch data again.

Incorrect attempts

I had these incorrect code snippets that listen to socket event and fetch data again

My first attempt was this, whenever there's any re-render, socket.on('USER_JOINED') would registers the same callback function passed into it so once the event USER_JOINED is emitted, the same callback will fire multiple times while we only need it to execute once to reduce api calls and performance obviously.

const Expense = () => {
  const [initialValues, setInitialValues] = useState(null);
  const [expense, setExpense] = useState(null);
  const { socket, toLogIn } = useContext(SWContext);
  // ...
  socket.on('USER_JOINED', (socketData) => {
    // fetch data again
  });
}

Enter fullscreen mode Exit fullscreen mode

My second attempt was to register the callback once when the component is mounted but I still experienced multiple callback executions. It's because even after the component is unmounted, the callback is still registered with socket (I use one single instance of socket (Singleton pattern)). And I wouldn't have access to new state if the state was updated.

  useEffect(() => {
    socket.once('USER_JOINED', (socketData) => {
      // fetch data again
    });
  }, []);
Enter fullscreen mode Exit fullscreen mode

Solution

This is what works for me so far. I register a socket event handler and a clean up socket.off every time expense changes. This way there's only one socket event handler being called at a time and detached when not needed

  useEffect(() => {
    socket.once('USER_JOINED', (socketData) => {
      // fetch data again
    });
    return socket.off('USER_JOINED');
  }, [expense]);
Enter fullscreen mode Exit fullscreen mode

I imagine this practice can also apply for similar situations like window.addeventlistener()


Take a look at my project if you're curious https://github.com/TueeNguyen/SplitWise3

Top comments (4)

Collapse
 
brense profile image
Rense Bakker

Using socket.off like that will unbind all event listeners to that event throughout your app, not just in the component you use it in.

It's better to first define the callback for the event listener and bind/unbind that:

const myEventHandler = useCallback(socketData => {
  // do stuff...
});

useEffect(() => {
  socket.on('MY_EVENT', myEventHandler);
  return () => socket.off('MY_EVENT', myEventHandler);
}, []);
Enter fullscreen mode Exit fullscreen mode
Collapse
 
tuenguyen2911_67 profile image
Tue

This is a great point, this could prevent unbinding all event handlers.

Collapse
 
sainig profile image
Gaurav Saini

Nice article 👍
Just an FYI, when you’re not doing heavy real-time stuff like multi person chat rooms or something like that you can use Server Sent Events instead a dedicated library like socket.io.
It’s an inbuilt HTML 5 feature and very suitable for lightweight real time apps.
Here’s a little something to get you started - digitalocean.com/community/tutoria...

Collapse
 
tuenguyen2911_67 profile image
Tue

Thanks, I'll check it out