简体   繁体   English

为什么在删除另一个组件中的所有侦听器后,socket.io 无法在 React 组件上进一步工作?

[英]Why socket.io does not work further on react components after I remove all listeners in another component?

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.经过数周的调试并不得不实施一个糟糕的解决方法来让我的 React 应用程序正常工作,我刚刚发现了这个问题,作为 React 的初学者,这让我感到困惑,所以我发布了这个问题来听取您的建议。

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.我有一个 react 应用程序,或者说是 Ionic react 应用程序(但它实际上与普通的 react web 应用程序相同),我使用著名的socket.io库与后端通信并实时接收消息。

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.我传递套接字对象是为了侦听子组件中的事件,直到有一天我注意到如果用户删除了 UI 中的 Component2 之一,那么我会收到 socket.io 收到的警告一个事件,但组件已经卸载,它会导致内存泄漏。 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.谷歌搜索后,我发现 socket.io 有一个内置函数来执行此操作,即我在重置函数中调用的socket.removeAllListeners() 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.但是,CustomComponent(应用程序中的最后一个组件)中的socket.on调用不再起作用。 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.如果我在重置函数中注释socket.removeAllListeners()行,则 CustomComponent 中的socket.on调用将再次开始侦听和接收消息。

Surprisingly, this does not work only with the last component in my app, which is the CustomComponent .令人惊讶的是,这不仅适用于我的应用程序中的最后一个组件,即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 .正如您在代码中看到的,我将reset函数作为道具传递给Component1 ,因此它与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.我实施的解决方法是将 CustomComponent 中的socket.on函数移动到socket.on内,以便在 ComponentDidMount 和 ComponentDidUpdate 发生时始终触发它。 The catch here is that the socket.on fires more than one time.这里的问题是 socket.on 触发不止一次。 So if I receive a message from server then I see in the browser that the function get called 5 times in a row.因此,如果我从服务器收到一条消息,那么我会在浏览器中看到该函数被连续调用 5 次。

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. socket.removeAllListeners()将从套接字中删除所有侦听器,包括由仍在安装和侦听的组件添加的侦听器。 A component should call socket.on when it mounts and socket.off when it unmounts.一个组件应该叫socket.on它安装时和socket.off当它卸载。 This can be achieved by using useEffect:这可以通过使用 useEffect 来实现:

const [configs, setConfigs] useState([]);

useEffect(() => {
    const onInit = configs => setConfigs(configs);

    socket.on('onInit', onInit);

    socket.emit('init', "initialize me");

    // return a function that unsubscribes the handler from the socket
    // you have to pass the handler which you passed to socket.on before to only remove that handler
    return () => socket.off('onInit', onInit);
}, []);

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.当组件仅订阅特定事件时调用socket.removeAllListeners()非常容易出错。 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.不把属于一起的副作用放在一个地方会让你很头疼。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM