无法移除React组件添加的Electron IPC监听器问题求助
问题:React组件卸载时无法移除Electron IPC监听器
在React组件挂载时注册Electron IPC监听器,卸载时尝试移除,但监听器引用不匹配导致移除失败。具体表现为:组件内验证handler引用一致,但preload中传入的handler与原引用不匹配,移除后监听器数量仍为1。
React组件简化代码
import React, { useEffect } from 'react'; import { useNavigate, useLocation } from 'react-router-dom'; import { useAppDispatch } from '../../redux/storeHooks'; import { setAuthToken } from './state/authSlice'; import { LocationAuthState } from '../common/RequireAuth/types'; export default () => { const dispatch = useAppDispatch(); const navigate = useNavigate(); const navigatedFrom = useNavigatedFrom(); useEffect(() => { const userLoggedInHandler = (_: Electron.IpcRendererEvent, loginToken: string) => { dispatch(setAuthToken({ token: loginToken })); navigate(navigatedFrom, { replace: true }); }; const ref1 = userLoggedInHandler; window.ipc.startLoginListenerHttpServer(); window.ipc.userLoggedIn(userLoggedInHandler); return () => { const theSame = ref1 === userLoggedInHandler; console.log(theSame); // 输出true window.ipc.stopLoginListenerHttpServer(); window.ipc.removeUserLoggedInListener(userLoggedInHandler); }; }, []); return ( <div> {`Waiting for login result...`} </div> ); }; function useNavigatedFrom() { const location = useLocation(); const locationState = location.state as LocationAuthState; return locationState?.from?.pathname || `/`; }
preload.js代码
import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron'; import IpcEvent from '../ipcEvent'; let originalHandlerReference: any; contextBridge.exposeInMainWorld(`ipc`, { startLoginListenerHttpServer: () => ipcRenderer.send(IpcEvent.StartLoginListenerHttpServer), stopLoginListenerHttpServer: () => ipcRenderer.send(IpcEvent.StopLoginListenerHttpServer), userLoggedIn: (handler: (event: IpcRendererEvent, loginToken: string) => void) => { originalHandlerReference = handler; ipcRenderer.on(IpcEvent.UserLoggedIn, handler); }, removeUserLoggedInListener: (handler: (event: IpcRendererEvent, loginToken: string) => void) => { const countBefore = ipcRenderer.listenerCount(IpcEvent.UserLoggedIn); // 1 const theSame = originalHandlerReference === handler; // false const theSameAsOriginal = originalHandlerReference === ipcRenderer.listeners(IpcEvent.UserLoggedIn)[0]; // true const theSameAsCurrentHandler = handler === ipcRenderer.listeners(IpcEvent.UserLoggedIn)[0]; // false ipcRenderer.off(IpcEvent.UserLoggedIn, handler); const countAfter = ipcRenderer.listenerCount(IpcEvent.UserLoggedIn); // 1 }, });
解决方案
问题根源是Electron的contextBridge会对传入的函数进行序列化/克隆,导致渲染进程中传入的handler和preload中接收的handler不是同一个引用,卸载时传入的新克隆handler自然无法匹配注册时的原handler。
有两种可行的修复方式:
方式1:在preload中维护监听器引用,移除时直接用原引用
修改preload.js,移除方法不再接收外部handler,而是直接使用之前保存的originalHandlerReference:
import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron'; import IpcEvent from '../ipcEvent'; let originalHandlerReference: ((event: IpcRendererEvent, loginToken: string) => void) | null = null; contextBridge.exposeInMainWorld(`ipc`, { startLoginListenerHttpServer: () => ipcRenderer.send(IpcEvent.StartLoginListenerHttpServer), stopLoginListenerHttpServer: () => ipcRenderer.send(IpcEvent.StopLoginListenerHttpServer), userLoggedIn: (handler: (event: IpcRendererEvent, loginToken: string) => void) => { originalHandlerReference = handler; ipcRenderer.on(IpcEvent.UserLoggedIn, handler); }, removeUserLoggedInListener: () => { if (originalHandlerReference) { ipcRenderer.off(IpcEvent.UserLoggedIn, originalHandlerReference); originalHandlerReference = null; // 清理引用,避免内存泄漏 } }, });
然后修改React组件中的卸载逻辑,调用移除方法时不再传handler:
return () => { window.ipc.stopLoginListenerHttpServer(); window.ipc.removeUserLoggedInListener(); // 无需传handler };
方式2:使用唯一标识管理监听器(支持多监听器场景)
如果需要支持多个监听器,可以给每个监听器分配唯一ID,preload中通过ID映射维护引用:
// preload.js import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron'; import IpcEvent from '../ipcEvent'; const handlerMap = new Map<string, (event: IpcRendererEvent, loginToken: string) => void>(); contextBridge.exposeInMainWorld(`ipc`, { startLoginListenerHttpServer: () => ipcRenderer.send(IpcEvent.StartLoginListenerHttpServer), stopLoginListenerHttpServer: () => ipcRenderer.send(IpcEvent.StopLoginListenerHttpServer), userLoggedIn: (handler: (event: IpcRendererEvent, loginToken: string) => void, id: string) => { handlerMap.set(id, handler); ipcRenderer.on(IpcEvent.UserLoggedIn, handler); }, removeUserLoggedInListener: (id: string) => { const handler = handlerMap.get(id); if (handler) { ipcRenderer.off(IpcEvent.UserLoggedIn, handler); handlerMap.delete(id); } }, });
React组件中生成唯一ID(使用React的useId钩子):
import { useId } from 'react'; export default () => { const listenerId = useId(); const dispatch = useAppDispatch(); const navigate = useNavigate(); const navigatedFrom = useNavigatedFrom(); useEffect(() => { const userLoggedInHandler = (_: Electron.IpcRendererEvent, loginToken: string) => { dispatch(setAuthToken({ token: loginToken })); navigate(navigatedFrom, { replace: true }); }; window.ipc.startLoginListenerHttpServer(); window.ipc.userLoggedIn(userLoggedInHandler, listenerId); return () => { window.ipc.stopLoginListenerHttpServer(); window.ipc.removeUserLoggedInListener(listenerId); }; }, [listenerId, dispatch, navigate, navigatedFrom]); return ( <div> {`Waiting for login result...`} </div> ); }; function useNavigatedFrom() { const location = useLocation(); const locationState = location.state as LocationAuthState; return locationState?.from?.pathname || `/`; }
内容的提问来源于stack exchange,提问作者Rychu
相关产品推荐
相关产品推荐

