NodeJS events triggering multiple times in electron-react app - javascript

I have a package (Let's say PACKAGE_A) written to do some tasks. Then it is required by PACKAGE_B. PACKAGE_A is a node script for some automation work. It has this Notifier module to create and export an EventEmitter. (The Whole project is a Monorepo)
const EventEmitter = require('events');
let myNotifier = new EventEmitter();
module.exports = myNotifier;
So in some functions in PACKAGE_A it emits event by requiring myNotifier, and also in the index.js of PACKAGE_A, I export functions (API exposed to the other packages) and the myNotifier by requiring it again.
const myNotifier = require('./myNotifier);
const func1 = () => {
// some function
return something;
}
module.exports = {func1, myNotifier}
Then I import the PACKAGE_A in PACKAGE_B and use the API functions exposed with the notifier. PACKAGE_B is an electron app with a React UI.
Below is how the program works.
I have a console output window in the electron app (React UI, UI_A). <= (keep this in mind)
When I click a button in UI_A it fires a redux action (button_action). Inside the action, a notification is sent to an event which is listened in the electron code using ipcRenderer.
ipcRenderer.send('button-clicked', data); // <= this is not the full code of the action. It's bellow.
Then in the electron code (index.js), I require another file (UI_A_COM.js which houses the code related to UI_A in electron side). The reason is code separation. Here's part of the code in index.js related to the electron.
const ui_a_com = require('./electron/UI_A_COM');
const createWindow = () => {
mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true,
},
resizable: false,
});
mainWindow.loadURL('http://localhost:3000');
const mainMenu = Menu.buildFromTemplate(menuTemplate);
ui_a_com (mainWindow);
};
Alright. Then in UI_A_COM.js, I listen to that triggered event button-clicked.
ipcMain.on('button-clicked', someFunction);
which runs the code from PACKAGE_A and return a result. So now when PACKAGE_A runs, it emits some events using myNotifier. I listen to them in the same file (UI_A_COM.js), and when those events are captured, I again send some events to React UI, which is subscribed when button_action fired.
myNotifier.on('pac_a_event_a', msg => {
mainWindow.webContents.send('ui_event_a', msg); // code in `UI_A_COM.js`
});
Here's the full code for the action. (Did not provide earlier because you'll get confused)
export const buttonAction = runs => {
return dispatch => {
ipcRenderer.send('button-clicked', data);
ipcRenderer.on('ui_event_a', (event, msg) => {
dispatch({ type: SOME_TYPE, payload: { type: msg } });
});
};
};
This will show the msg in the UI_A console.
So this is the task I'm doing. The problem is when I click the button; it works perfectly for the first time. But when I click the button on the second time, it received two messages. Then when I click the button again, three messages and it keeps growing. (but the functions in the PACKAGE_A only executes one time per button press).
Let's say the message from PACKAGE_A emitted is 'Hello there' per execution.
When I press the button 1st time a perfect result => Hello there, When I click the button again => Hello there Hello there, When I click it again => Hello there Hello there Hello there.
It's kept so on. I think my implementation of EventEmitter has some flows. So why it's happening like this? Is it EventEmitter or something else? What am I doing wrong here?

By default the electron-react boilerplate doesnt define the ipcRenderer.removeAllListeners method. So you have to first go to the main/preloads.ts file and add them :
removeListener(channel: string, func: (...args: unknown[]) => void) {
ipcRenderer.removeListener(channel, (_event, ...args) => func(...args));
},
removeAllListeners(channel: string) {
ipcRenderer.removeAllListeners(channel);
},
Then go to the renderer/preload.t.s declaration file and add the declarations too:
removeListener(
channel: string,
func: (...args: unknown[]) => void
): void;
removeAllListeners(channel: string): void;
After that make sure to clean all listeners in the cleanup function of your useEffects each time you listen to an event fired. This will prevent multiple firing.
useEffect(() => {
window.electron.ipcRenderer.on('myChannel', (event, arg) => {
// do stuffs
});
return () => {
window.electron.ipcRenderer.removeAllListeners('myChannel');
};
});

I think you should return a function that call ipcRenderer.removeAllListeners() in your component's useEffect().
Because every time you click your custom button, the ipcRenderer.on(channel, listener) is called, so you set a listener to that channel agin and agin...
Example:
useEffect(() => {
electron.ipcRenderer.on('myChannel', (event, arg) => {
dispatch({ type: arg });
});
return () => {
electron.ipcRenderer.removeAllListeners('myChannel');
};
});

Related

Listening to a CustomEvent from another file

I'm dealing with some legacy code which is dispatching a custom event like so:
class AuthStatus {
constructor() {
this.init();
}
async init() {
const event = new CustomEvent("qwerty-authentication", {
detail: {
isLoggedIn,
sessionId,
submittedEmail,
user,
},
});
window.dispatchEvent(event);
}
}
export default AuthStatus;
But how can I listen for the event triggering from another file? Is that even possible? Everytime I run: window.addEventListener('qwerty-authentication', (e) => console.log(e)); it's always undefined, but if I run it in the same file I do get the values.
You need to use addEventListener to listen to that event, as the event applied to the window window.dispatchEvent(event);, so you need.
window.addEventListener('qwerty-authentication', function () {
console.log('event fired');
})

How to use the `PerformanceNavigationTiming` API to get page load time?

I am trying to to use the PerformanceNavigationTiming API to generate a page load metric.
The MDN API document linked above says that the PerformanceEntry.duration should give me what I need because it:
[r]eturns a timestamp that is the difference between the PerformanceNavigationTiming.loadEventEnd and PerformanceEntry.startTime properties.
However, when I check this property, I get simply 0. I'm accessing this API from within a React hook that runs a useEffect function that wait for the window load event and then checks the api like so:
export const useReportPageLoadTime = () => {
useEffect(() => {
const reportTime = () => {
let navPerformance: PerformanceEntry
navPerformance = window.performance.getEntriesByType('navigation')[0]
console.log({
duration: navPerformance.duration,
blob: navPerformance.toJSON()
})
}
if (document.readyState === 'complete') {
reportTime()
return null
} else {
window.addEventListener('load', reportTime)
return () => window.removeEventListener('load', reportTime)
}
}, [])
}
As you can see there, I also call toJSON on the performance entry and indeed it shows that the values upon which duration (startTime and loadEventEnd) are both 0 as well:
Does anyone know why I am getting this value?
I was finally able to get this to work using a different method than the event listener. It certainly is logical that the data should be ready when the load event fires, but the only way I was able to get the data was to use another feature of the Performance API: the PerformanceObserver, which fires a callback when a new piece of data has become available.
Here is the code that worked for me:
export const useReportPageLoadMetrics = () => {
useEffect(() => {
const perfObserver = new PerformanceObserver((observedEntries) => {
const entry: PerformanceEntry =
observedEntries.getEntriesByType('navigation')[0]
console.log('pageload time: ', entry.duration)
})
perfObserver.observe({
type: 'navigation',
buffered: true
})
}, [])
}

How to reset Cypress window.location.href after test

I am testing some ui that on click updates the window.location.href. I have two tests, the first one works, but the second starts in the location set by the ui in the previous test. This is wrong and stops test two from starting on the right page.
How can I reset the window.location.href or just the Cypress browser location in general back to where it was at the beginning of the first test?
I have checked the window.location.href at the start of the second test and it looks autogenerated and so I don't think wise to try and hardcode that value into window.location.href at the start of the second test.
Looking for something I can run at afterEach.
Test
it.only('should send asynchronous analytics event after provider selection click', () => {
rewire$useFlag(() => true);
cy.location().then((location) => console.log('window !!'));
const analyticsAsyncStub = cy.stub().as('sendAnalyticsAsyncStub');
rewire$sendAnalyticsAsync(analyticsAsyncStub);
// #NOTE hacking browser detection so required provider options are availiable
cy.window().then(($window) => {
console.log('window !!', $window.location.href);
($window as any).chrome = {};
($window as any).chrome.runtime = {
sendMessage() {
'mock function';
},
};
});
mountFixtureWithProviders({ children: <ProviderSelection flagsConfig={defaultFlags} /> });
cyGetByTestId('provider--metaMask').click();
cy.get('#sendAnalyticsAsyncStub').should(
'have.been.calledWithMatch',
analyticsUtilsModule.createButtonEvent(ButtonEventName.providerSelectionPressed),
);
});
mountFixtureWithProviders function
export const mountFixtureWithProviders = ({
children,
mountInsideVisualMock = true,
setErrorLog = () => ({}),
renderErrorScreens,
}: {
children: ReactNode;
mountInsideVisualMock?: boolean;
setErrorLog?: SetErrorLog;
renderErrorScreens?: boolean;
}) => {
const RouterMockedChildren = () => <MemoryRouter>{children}.
</MemoryRouter>;
const ProvidedChildren = () =>
renderErrorScreens ? (
<DemoAppToRenderErrorMessages>
<RouterMockedChildren />
</DemoAppToRenderErrorMessages>
) : (
<LinkUiCoreContext.Provider value={{ setErrorLog,
imageResizerServiceUrl: DEV_IMAGE_RESIZER_SERVICE_URL }}>
<RouterMockedChildren />
</LinkUiCoreContext.Provider>
);
return mount(mountInsideVisualMock ? linkPageVisualMock({
children: <ProvidedChildren /> }) : <ProvidedChildren />);
};
Thank you.
Usually you can just do a visit in a beforeEach() to get a clean start for each test
beforeEach(() => {
cy.visit('/')
})
There's also cy.go('back') which you can run at the end of the first test.
But be aware that a fail in test 1 will then fail test 2 because the navigation won't happen - same applies to adding into afterEach().
In Cypress window is the test runner window, but you can access the app window with
cy.window().then(win => console.log(win.location.href))
or the location directly with
cy.location().then(loc => console.log(loc.href))
Don't use cy.log() for debugging, use console.log() as there are side-effects to cy.log() that may give you the wrong debugging info.
(mountFixtureWithProviders({ children: <ProviderSelection flagsConfig={{}} /> }) looks like a component test, so yes I agree that should give you a fresh start for each test.
Can you add the two tests and also mountFixtureWithProviders in the question to give the full picture please.

How to correctly add event listener to React useEffect hook?

I am trying to add an event listener to an Autodesk Forge viewer. This is an application built on React and this is the code I am trying:
const selectEvent = () => {
let viewer = window.NOP_VIEWER;
viewer.addEventListener(Autodesk.Viewing.SELECTION_CHANGED_EVENT, (e) => {
setSelection(e.dbIdArray);
});
};
This runs perfectly when called from a button onClick:
<Button onClick={() => selectEvent()}>Add</Button>
However, I would like the event listener to turn on when the page is loaded, so I tried useEffect:
useEffect(() => {
let viewer = window.NOP_VIEWER;
if (viewer) {
selectEvent();
}
}, []);
Even after trying some modifications, I could not get it to work. Nothing happens, so I suspect the event listener never gets added. Looking around at other solutions, event listeners are usually loaded with useEffect, but I am not sure what I am doing wrong. Any tips would be appreciated!
edit: It does enter the if statement, as a console.log works
Some background (might be relevant):
The viewer is loaded from a useEffect
useEffect(() => {
initializeViewer(props);
}, []);
and the viewer can be accessed as shown in the code above.
Try some thing like this.
When ever change in viewer and viewer is available, then you register the event.
Deregister the event handler as return function to hook
useEffect(() => {
if (viewer) {
viewer.addEventListener(Autodesk.Viewing.SELECTION_CHANGED_EVENT, (e) => {
setSelection(e.dbIdArray);
});
}
return () => { /* do the removeEventLister */ }
}, [viewer]);
Try this
NOP_VIEWER is a global variable to access the current Viewer
you need to remove the event listener after listening otherwise it will cause memory leak
useEffect(()=>{
NOP_VIEWER.addEventListener(Autodesk.Viewing.SELECTION_CHANGED_EVENT, (e) => {
setSelection(e.dbIdArray);
});
return()=>{NOP_VIEWER.removeEventListener(Autodesk.Viewing.SELECTION_CHANGED_EVENT, (e) => {
setSelection(e.dbIdArray);
}))}
},[])
or if it doesn't work
useEffect(()=>{
let viewer= window.NOP_VIEWER
viewer.addEventListener(Autodesk.Viewing.SELECTION_CHANGED_EVENT, (e) => {
setSelection(e.dbIdArray);
});
},[])

How to trigger an event in external js file?

This is app.js file;
const MyTest = require('./testApp');
const App = new MyTest();
App.on('ready', () => {
console.log('Hello World');
});
Package.json file. (in) ./testApp/package.json;
{
...
"main": "main.js"
...
}
This is ./testApp/main.js file;
const EventEmitter = require('events'); // node event module
class Client extends EventEmitter {
constructor() {
super();
this.sendReady();
}
sendReady() {
this.emit('ready');
}
}
module.exports = Client;
If "App" is successfully installed in the app.js file, I want the console to write " Hello World". I've changed a few times this content because I want to more simple.
I want to trigger App.on in app.js from ./testApp/main.js
If I define a function in the constructor, I can get it by calling "App." In this case this = App
Then, why don't emit anything?
Note: this app files working locally.
The emit call is in Client's constructor, i.e. it gets called when you invoke
const helper = new pulseHelper.Client();
However, you only register the ready handler after that. EventEmitters don't buffer their events; only listeners registered during the emit call will ever be able to act on the events.
EDIT: to clarify what I'm talking about, see this example:
const EventEmitter = require('events');
class Client extends EventEmitter {
constructor() {
super();
this.emit('ready', 'I\'m Ready!');
}
boom() {
this.emit('ready', 'Boom!');
}
emit(event, payload) {
console.log('Emitting', event, ':', payload);
super.emit(event, payload);
}
}
const c = new Client();
c.on('ready', (payload) => console.log(payload));
c.boom();
This prints (with annotations):
# client constructed, it emits an event but no one is listening.
# however the "debug" emit() implementation prints this:
Emitting ready : I'm Ready!
# the listener has been registered, now let's call .boom()!
# the "debug" emit() implementation prints this:
Emitting ready : Boom!
# and the handler prints the following:
Boom!

Categories