You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

无法移除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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.14 11:25:30