'Socket.io connection intact but React component that never unmounts becomes unresponsive to socket events after page navigation

Setup:

  • Socket.io in React with react-router-dom
  • Socket instance passed to child components via useContext
  • Backend built using Express

Problem:
Navbar listens for socket events to update counters for unread messages. Everything works fine until user navigates to a different page. Once a navigation occurs, the socket in navbar (which never unmounts) is unresponsive to future events being emitted from the server even though the connection is intact.

Expected behaviour:
Navbar socket remains responsive to emits from server after page navigation.

Observation:
Other areas of the app where socket is utilized remains functional even while navbar socket is unresponsive. The navbar socket becomes responsive again when page is refreshed but the same problem repeats after page navigation. The navbar is the only component that doesn't unmount when a navigation occurs while components mounted on navigation is loaded with the socket instance via useContext.

As far as I can see, connection is never broken (disconnect event never fires on server-side) and the newly mounted component can emit an event, server responds, and emits a response back to client where the new component responds while the navbar doesn't.

Other notes:
This is the first question I've ever asked so my apologies in advance if the question is poorly formatted. The code is obviously simplified to leave out areas where it seems to not affect the problem. The server-side code is omitted because it seems to receive and emit events without any problems.

//App.js

const App = () => {
  const socket = useRef(io('path'));

  return (
    <SocketContext.Provider value={socket.current}>
      <BrowserRouter>
        <Nav />
        <Switch>
          <Route path='/messages' component={Messages} />
          <Route path='/' component={Home} />
          <!-- Other routes -->
        </Switch>
      </BrowserRouter>
    </SocketContext.Provider>
  )
}
//Nav.js

const Nav = () => {
  const socket = useContext(SocketContext);

  const [newMsgCount, setNewMsgCount] = useState(0);

  useEffect(() => {
    socket.emit('get new msg count');

    socket.on('new msg count', (count) => setNewMsgCount(count));

    socket.on('new msg received', () => setNewMsgCount(prev => prev + 1));

    socket.on('marked as read', () => setNewMsgCount(prev => prev - 1));

    return () => {
      socket.off('new msg count');
      socket.off('new msg received');
      socket.off('marked as read');
    }
  }, [socket]);

  return (
    //Omitted for brevity
  )
}
//Messages.js

const Messages = () => {
  const socket = useContext(SocketContext);

  const [msgs, setMsgs] = useState([]);
  const [newMsg, setNewMsg] = useState({ to: '', body: '' });

  useEffect(() => {
    socket.emit('get all msgs');

    socket.on('all msgs', (data) => setMsgs(data));

    socket.on('new msg received', (data) => setMsgs(prev => ([...prev, data])));

    return () => {
      socket.off('all msgs');
      socket.off('received new msg');
    }
  }, [socket]);

  const send = () => socket.emit('new msg', newMsg);

  return (
    <div>
      <!-- Omitted for brevity -->
      <form onSubmit={send}>
        <!-- Omitted for brevity -->
        <button>Send</button>
      </form>
    </div>
  )
}


Solution 1:[1]

Since your socket wants to live from first page load until you close the page (I assume so..) I suggest to decouple socket initialisation from React. It just doesn't seem right to put it in a ref because even the app component has still a chance to unmount and mount again (as you experienced on page load) or to do other sideeffects to it.

For example have a file socket.js

import React from "react";
import io from "socket.io-client";

// have a standalone variable holding the socket.
const path = '...'
export const socket = io(path);
export const SocketContext = React.createContext(socket);

Then in your APP.js do like so:

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

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

Or use it in any other file my-outsourced-mapping.js

import { socket } from "./socket";

// say we have redux
import store from './store'
export const veryLargeMappingFunctionForThisParticularShittyEventMyBackendGuyDid() {
   socket.on('complex-data-event', data => {
        
        mapped = // reduce, map, group and more
        store.dispatch('simple-data-event', mapped)
   })
}

You can adapt this to maybe only initialise socket after login is succeded..

I have this in plenty of projects and I never experienced such headaches problems with this solution - KEEP IT SIMPLE.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1