DEV Community

bravemaster619
bravemaster619

Posted on • Updated on

How to prevent multiple socket connections and events in React

Multiple socket connections and event bindings can be easily overlooked in SPAs and can cause headaches. I'd like to share my experience of preventing them. In this article, I will use socket.io for Node.js backend and socket.io-client for React frontend.

1. Connect only once

Create a dedicated file for socket connection. For example, create a file in service/socket.js:


import io from "socket.io-client";
import { SOCKET_URL } from "config";

export const socket = io(SOCKET_URL);
Enter fullscreen mode Exit fullscreen mode

You can import this socket instance in other React components whenever necessary:

import {socket} from "service/socket";

function MyComponent() => {
   return(<div></div>)
}
Enter fullscreen mode Exit fullscreen mode

In this way, you can ensure there will be only one socket instance.

2. Bind and emit events only once - put them in the correct place

Consider you're building a React component that connect to a socket server when the page is loaded and show welcome message if the connection is successful. The component will emit an HELLO_THERE event to the server and the server will respond with warm WELCOME_FROM_SERVER event. Where would you place socket event binding and emiting in your React code?

import {socket} from "service/socket";
// NOT HERE (1)
// socket.emit('HELLO_THERE');
function MyComponent() => {

   // NOT HERE EITHER (2)
   // socket.emit('HELLO_THERE');
   const [connected, setConnected] = useState(false);
   // IT IS HERE
   useEffect(() => {
      socket.emit('HELLO_THERE');
      const eventHandler = () => setConnected(true);
      socket.on('WELCOME_FROM_SERVER', eventHandler);
      // unsubscribe from event for preventing memory leaks
      return () => {
         socket.off('WELCOME_FROM_SERVER', eventHandler);
      };
   }, []);
   return (
      <div>
          { connected ? (
               <p>Welcome from server!</p>
          ) : (
               <p>Not connected yet...</p>
          ) }
      </div>
   )
}
export default MyComponent
Enter fullscreen mode Exit fullscreen mode
  • (1) This will emit the event even if MyComponent is not used.
  • (2) This will emit the event whenever MyComponent is updated.

* If you're using React classes, componentDidMount will be the right place to use sockets. useEffect(() => {}, []) is almost the same as componentDidMount
** Of course, socket.emit can go anywhere necessary in your code. However, socket.on should be in componentDidMount in most cases.

3. If you're unsure, there is always this hack

If you still cannot figure out why does your component stupidly bind the same listener for multiple times, or if you just want to make sure bind only one listener for one event, you can resort to socket.off.

socket.off('MY_EVENT', doThisOnlyOnce).on('MY_EVENT', doThisOnlyOnce);
Enter fullscreen mode Exit fullscreen mode

UPDATE - useContext approach

If you want to use useContext hook, you can do like this one:

socket.js

import React from "react";
import io from "socket.io-client";
import { SOCKET_URL } from "config";

export const socket = io(SOCKET_URL);
const SocketContext = React.createContext(socket);
Enter fullscreen mode Exit fullscreen mode

Your app.js should look like the following:

import React from "react";
import SocketContext, { socket } from "context/socket";

const App = () => (
   <SocketContext.Provider value={socket}>
      <OtherComponents />
   </SocketContext.Provider>
);
Enter fullscreen mode Exit fullscreen mode

Then anywhere in your component, you can get socket like this:

import React from "react";
import SocketContext from "context/socket";

const OtherComponent = () => {
  const socket = React.useContext(SocketContext);
  ...
};
Enter fullscreen mode Exit fullscreen mode

For more information, you can read my another post here:

Top comments (12)

Collapse
 
the_riz profile image
Rich Winter

I was working with React and Socket.io and one of the warning messages I got from the React server suggested storing the socket reference in a useRef() hook, but honestly, I kind of like your solution more.

Another additional thought I've seen is to use the .context() API and serve the socket to all components through a Provider.

Collapse
 
blouzada profile image
Bruno Louzada

@bravemaster619 I'm trying to use this strategy with a complex react app, with authentication, tests, I think i solved authentication but the tests are impossible, did you used this with a complex app or just for simple apps? can you help me?

Collapse
 
bravemaster619 profile image
bravemaster619 • Edited

Sorry for late reply. I see no reason why tests are impossible in this approach. Can you show me some of your code?

Collapse
 
blouzada profile image
Bruno Louzada • Edited

I implemented socket io as middleware, my problem with socket in component and tests is mocking socket io with jest.mock, I dont want to connect to real server in tests, but with middleware approach I could make tests and it seems better to not depend on component life cycle to handle events;
I had a really hard time configuring websocket with React, with middleware approach I could met all requirements

  • 1 connection per client
  • Connect based on users only after login
  • Tests
  • Update application state
  • Show snackbars
  • Change page based on events
Collapse
 
mynameistakenomg profile image
Sean_Fang

It is really a great helpful article, many thanks!🙏 I have been recently working with socket io( totally a layman 😂...), and just encountered the issue of multiple listeners attached to a certain event, even I put my socket in my useEffect, the problem still persisted, in the end, it turned out I didnt do cleanup in useEffect. After that, I experimented on it a bit, and found out that those listeners are still there even after the connection is closed, thus next time when a user log back in( in my case), the user wil have multiple listeners fire for a single event. Not quite sure if this is default behavior, but its kinda weird cuz I thought everything will be gone after the connection is closed😅. And one more thing, I personally think that "off() & on()" trick is the safest way to work with socket in react cuz we usually use socket across components, definitely dont wanna trigger the event in multiple places...
Again, thank you for such a great article👍

Collapse
 
itminhnhut profile image
itminhnhut • Edited

if i have MyComponent and MyComponent2 use socket.on('receive_event', eventHandler);
if MyComponent2 use socket.off('receive_event').on('receive_event',()=> function)
is MyComponent have is off after on like MyComponent2 ? .
i want MyComponent2 change socket on is MyComponent no action . How ?.

Collapse
 
zafarsaleem profile image
Zafar Saleem • Edited

Shouldn't this be const eventHandler = () => setConnected(true); like this const eventHandler = () => setConnected(!connected);

Collapse
 
pxsty0 profile image
Mustafa "Pxsty" KÖK

WHAT I AM LOOKING FOR IS FINALLY!!

Collapse
 
arslan3693 profile image
Arslan Ahmed

It worked. Thanks.

Collapse
 
ziat1988 profile image
ziat1988

Hello,
What is the difference between the two solutions you mentioned? The service and the contextApi?

Collapse
 
gaohomway profile image
高宏伟

why are you not executing the connect event ? SocketIO.on('connect', () => { ... })

Collapse
 
gaohomway profile image
高宏伟

Where do the connection events need to be placed?