How can one prevent excess JS event handlers in React? - javascript

Problem
An application requires the inner size of the window. React patterns suggests registering an event listener within a one-time effect hook. The call to window.addEventListener appears to occur only once, but event listeners pile up and negatively affect performance.
Code
Here's the pared down source code that reproduces this issue
import React, {useState, useEffect} from 'react';
const getWindowRect = () => {
return new DOMRect(0, 0, window.innerWidth, window.innerHeight);
}
// custom hook to track the window dimensions
const useWindowRect = () => {
// use state for the aspect ratio
let [rect, setRect] = useState(getWindowRect);
// useEffect w/o deps should only be called once
useEffect(() => {
const resizeHandler = () => { setRect(getWindowRect()); };
window.addEventListener('resize', resizeHandler);
console.log('added resize listener');
// return the cleanup function
return () => {
window.removeEventListener('resize', resizeHandler);
console.log('removed resize listener');
}
}, []);
// return the up-to-date window rect
return rect;
}
const App = () => {
const window_rect = useWindowRect();
return <div>
{window_rect.width/window_rect.height}
</div>
};
export default App;
Testing
relevant console output reads:
added resize listener
this is the expected result where the listener is added only once, no matter how much the app is re-rendered
reference, window not resized max listeners: 56
resizing performance, hundreds of listeners accumulate max listeners: 900+
resizing performance w/ window.addEventListener commented out max listeners: 49
Environment
React 16.13.1
TypeScript 4.0.3
WebPack 4.44.2
Babel Loader 8.1.0
Chrome 86.0.4240.111 (Official Build) (x86_64)
Demo
Assuming is would be difficult to run performance metrics on a JSFiddle or CodePen I've provided a full demo at this repo: oclyke-exploration/resize-handler-performance You can easily run the demo as long as you have node and yarn installed.
General Discussion
this approach has worked before w/o these symptoms, however the environment was slightly different and did not include TypeScript (could this be caused by the cross-compilation?)
i've briefly looked into whether the function reference that is provided to window.removeEventListener is the same as that provided to window.addEventListener - though this should not even come into play when the effect only occurs once
there are many possible ways to work around this issue - this question is intended to ask why this method, which is expected to work, does not
reproduced this issue on a fresh create-react-app project using react-scripts 4.0.0
Ask
Does anyone have an explanation for this issue? I'm stumped!
(related: can others reproduce this issue?)

As pointed out by Patrick Roberts and Aleksey L. in comments on the question the issue is not actually an issue because:
the event handlers are registered by invokeGuardedCallbackDev in react-dom.development.js
the event handlers are cleaned up periodically
this does not affect production builds

Related

I had an issue today with window.addEventListener and React Native

I was trying to implement dynamic re-rendering of my react application using this function I found here:
https://stackoverflow.com/a/19014495/7838374
function useWindowSize() {
const [size, setSize] = useState([0, 0]);
useLayoutEffect(
() => {
function updateSize() {
setSize([window.innerWidth, window.innerHeight]);
}
window.addEventListener("resize", updateSize);
updateSize();
return () => window.removeEventListener("resize", updateSize);
},[]);
return size;
}
function ShowWindowDimensions(props) {
const [width, height] = useWindowSize();
return (
<span>
Window size: {width} x {height}
</span>
);
}
Link to my app:
https://github.com/Intetra/aubreyw
I was able to get everything working perfectly when displaying my app in the browser on my desktop. I'm using expo to run the app. The problem came when I tried to run the app on my android phone. I was getting an error at launch.
COMPONENT EXCEPTION: window.addEventListener is not a function
I was able to get it working with a solution I found here:
https://stackoverflow.com/a/61470685/7838374
That solution says that the event listener for window doesn't exist in react native, so we have to mock it. I don't understand what that means. I still don't know why the solution I found worked. I would like to understand. Can someone enlighten me?
The browser environment is different than the React Native environment in some ways, despite the fact that both use Javascript. This means that while the language is the same, some of the underlying features may be different, meaning they could behave differently, or exist in one place and not the other.
window.addEventListener is an example of something that we can expect to exist in the browser world, but is not implemented in React Native. This gets slightly more complicated, of course, by Expo, which allows running React Native code on the web by shimming certain features, trying to bridge some of the difference between the two worlds.
Because of the dynamic nature of Javascript, even though window.addEventListener isn't provided by React Native on iOS/Android, we can just add it to our environment by defining it ourselves. That's what the solution you found (window.addEventListener = x => x) does -- it just adds a function that doesn't actually do anything (it takes x as a parameter and returns x as a result). This is sometimes referred to as a mock -- you'll often see this in the testing world.
On React Native on the device, in my testing, your solution wouldn't produce an error, but it also wouldn't actually give you the dimensions. Luckily, you can use Dimensions to get the screen size, which Expo also exposes in the web version. So, this will looks to return the correct size on both the native app and the Expo web version:
function useWindowSize() {
const [size, setSize] = React.useState([0, 0]);
React.useLayoutEffect(
() => {
console.log("Layout effect")
function updateSize() {
setSize([Dimensions.get('window').width, Dimensions.get('window').height])
}
window.addEventListener("resize", updateSize);
updateSize();
return () => window.removeEventListener("resize", updateSize);
},[]);
return size;
}
Note, you should do some testing to see what happens when rotating, resizing, etc -- I only made sure the basic functionality works.
Here's a snack with the code in context: https://snack.expo.io/Mofut1jHa
Also note that window.removeEventListener has to be mocked as well, which you can see in the Snack.

Touch move is really slow

I'm building a paint-like feature where the user can draw a line, but the touchmove event gets emitted really slow on my device (android phone), so the line becomes edgy. As soon as I connect the device to my PC and open the chrome devtools via USB debugging, everything works fine. On the phone emulator in desktop-chrome aren't any problems.
Here is a screenshot. The inner circle was drawn with the slow touch events, and for the outer one I connected the device to my PC.
Here is another screenshot showing the durations between individual "touchmove" event-calls. The top part (green values) occured when the devtools were open, the bottom part (red values) when they were closed.
The code:
function DrawingCanvas(/* ... */) {
// ...
const handleTouchMove = (event) => {
handleMouseMove(event.touches[0])
}
const handleMouseMove = ({ clientX, clientY }) => {
if (!isDrawing) {
return
}
const canvasRect = canvas.getBoundingClientRect()
const x = clientX - canvasRect.x
const y = clientY - canvasRect.y
currentPath.current.addPoint([x, y])
update()
}
const update = () => {
clearCanvas()
drawPath()
}
// ...
useEffect(() => {
const drawingCanvas = drawingCanvasRef.current
// ...
drawingCanvas.addEventListener("touchstart", handleDrawStart)
drawingCanvas.addEventListener("touchend", handleDrawEnd)
drawingCanvas.addEventListener("touchcancel", handleDrawEnd)
drawingCanvas.addEventListener("touchmove", handleTouchMove)
drawingCanvas.addEventListener("mousedown", handleDrawStart)
drawingCanvas.addEventListener("mouseup", handleDrawEnd)
drawingCanvas.addEventListener("mousemove", handleMouseMove)
return () => {
drawingCanvas.removeEventListener("touchstart", handleDrawStart)
drawingCanvas.removeEventListener("touchmove", handleTouchMove)
drawingCanvas.removeEventListener("touchend", handleDrawEnd)
drawingCanvas.removeEventListener("touchcancel", handleDrawEnd)
drawingCanvas.removeEventListener("mousedown", handleDrawStart)
drawingCanvas.removeEventListener("mouseup", handleDrawEnd)
drawingCanvas.removeEventListener("mousemove", handleMouseMove)
}
})
return <canvas /* ... */ />
}
Does anyone have an idea on how to fix this?
You can test it by yourself on the website: https://www.easymeme69.com/editor
Somehow calling event.preventDefault() on the touchmove event fixed it.
I'm facing exactly the same situation, I'm developing a React app with some touch features implementing actions on touchmove.
All my tests are done inside Chrome on the Debian-based Raspberry OS distro.
It results in a deadly laggy UI with a real touch screen...except (this is when it becomes very interesting!) if the console is opened with Chrome mobile emulator, then even if I try to play with my finger on the real touch screen at this moment.
touch-action: none & event.stopPropagation hacks were already existing in my code and didn't change the game.
2 conclusions on that :
The touch screen (and its driver) is fine
The CPU is quite able to handle the load
As for now, the mystery is still opaque for me.
My feeling is that, somehow, Chrome is deliberately decreasing/increasing the touch events rate depending (correspondingly) on whether we're in a real use case or whether we're on the emulator. I created a simple fiddle to validate this hypothesis: https://jsfiddle.net/ncgtjesh/20/show
It seems to be the case since I can clearly that the emulator-enabled mode outputs 240 events/second while the real non-emulated interface is stuck to 120.
I'm quite surprised that the fixes enacted in the responses above made it since it seems to be a browser implementation choice.
I had this exact same thing happen to me, down to not being able to reproduce with USB debugging open. Besides the e.preventDefault() hack, you can also set the touchable element's touch-action: none; in CSS.
I've had the same problem. I had no freezes on mobile or firefox, only on Chromium. Either disabling touchpad-overscroll-history-navigation in chrome-flags or e.preventDefault() can solve the problem.

How to make page search on electron like in Chrome or VS Code or Firefox? [duplicate]

Does the Electron application framework have built-in text search?
The quick-start application doesn't provide any apparent search functionality (e.g. using Ctrl-F or from the menu options). I would have expected this to be a BrowserWindow option (or an option of its WebContents), but I don't see anything helpful in the docs.
I know this is an old thread, but might still be relevant for people out there.
Had the same problem, and first fixed by using electron-in-page-search, but this component doesn't work properly with Electron 2 or greater.
Then finally found electron-find resolved my problem. Using with Electron 4.
You just add the component to your project:
npm install electron-find --save
Add a global shortcut in your Electron main process to send an event to the renderer in a ctrl+f:
globalShortcut.register('CommandOrControl+F', () => {
window.webContents.send('on-find');
});
And then you can add this to your page (the renderer process)
const remote = require('electron').remote;
const FindInPage = require('electron-find').FindInPage;
let findInPage = new FindInPage(remote.getCurrentWebContents());
ipcRenderer.on('on-find', (e, args) => {
findInPage.openFindWindow()
})
Hope that helps.
Try webContents.findInPage just added in the latest version.
There is an issue with the solution Robson Hermes offered. globalShortcut is, by definition, global, so the shortcut will be detected even when the app is not focused. This will result in the Ctrl+F shortcut being "stolen" from everywhere else.
I have found no ideal solution (see this issue on the electron repository), but a hacky one can be achieved by doing what Robson said and adding
win.on('focus', () => {
globalShortcut.register('CommandOrControl+F', () => windows.main.send('on-find'))
})
win.on('blur', () => {
globalShortcut.unregister('CommandOrControl+F')
}
Note that as seen here, this is not ideal and can lead to several issues:
Other applications can get a lock on the shortcut when you lose focus, i.e. the shortcut will magically stop working when you switch back to the app later.
Some apps can appear on screen without taking focus (spotlight I believe has this behavior) and during the app's appearance the shortcuts will still be captured by your application.
There's also gonna be those weird one in a thousand situations where somehow you switch focus and the shortcut is not removed.
Instead of using global shortcuts , use Accelerators ( normal Keyboard shortcut )
{
label : 'help',
click : function(){.
electron.shell.openExternal('http://....').
},
accelerator : 'CmdOrCtrl+ Shift + H'
}
The above shown is just an example of How to use accelerator

react-testing-library: some portion of debug's output is not visible

I am using react jest with react testing library to test my component.
I am facing a weird issue. I am usng debug return by render from testing-library.
test('component should work', async () => {
const { findByText, debug } = render(<MyComponent />);
const myElement = await findByText(/someText/i);
debug();
});
As you can see in the screenshot there are incomplete dev and closing tags for parents are missing.
You need to change the value of DEBUG_PRINT_LIMIT env variable (default is 7000).
For example, run your tests with: DEBUG_PRINT_LIMIT=10000 yarn test
Source: https://github.com/testing-library/dom-testing-library/blob/master/src/pretty-dom.js#L33
I am using this version: "#testing-library/react": "^11.0.4"
able to do just like below, we can change 300000 as the max length of output.
debug(undefined, 300000)
Another option
screen.debug(undefined, Infinity);
The second argument of the debug() function can be used to set maxLengthToPrint.
E.g. to print everything in myElement using the recommended screen approach:
import {render, screen} from '#testing-library/react'
render(<MyComponent />);
const myElement = await screen.findByText(/someText/i);
const maxLengthToPrint = 100000
screen.debug(myElement, maxLengthToPrint);
See:
Api docs for debug() in render results
Api docs for screen.debug()
You can use Jest Preview (https://github.com/nvh95/jest-preview) to view your app UI when testing in a browser, instead of HTML in the terminal, just by 2 lines of code:
import { debug } from 'jest-preview';
describe('App', () => {
it('should work as expected', () => {
render(<App />);
debug();
});
});
It works great with jest and react-testing-library.
Introduction: https://www.jest-preview.com/docs/getting-started/intro
Installation guide: https://www.jest-preview.com/docs/getting-started/installation
If none of the other answers work for you, make sure it's not your terminal limit. For example VS Code only keeps 5000 lines in buffer. Try Mac OS terminal.
This worked for me:
debug(undefined, 300000);
It will give you the markeup of whatever you passed into render() as:
import {render, screen} from '#testing-library/react'
render(<MyComponent />);
You can read about more ways to help you with printing out the results, including prettifying the resulting markup at:
API doc for debug
This worked for me
const history = createMemoryHistory()
const { debug } = renderWithRedux(
<Router history={history}>
<SideBar />
</Router>
, state);
screen.debug(debug(), 20000);
Since the DOM size can get really large, you can set the limit of DOM content to be printed via environment variable DEBUG_PRINT_LIMIT. The default value is 7000. You will see ... in the console, when the DOM content is stripped off, because of the length you have set or due to default size limit. Here's how you might increase this limit when running tests:
DEBUG_PRINT_LIMIT=10000 npm test
Here more about debuggin on documentation
Adding on top of answer by #Haryono
Also make sure you don't have silent flag set in scripts
"test": "jest --config jest.config.js --silent";
Removing silent flag should work.
Note: I think silent flag supresses warnings and debug outputs
Also be sure your terminal allows you to scroll back that far. In iTerm2, I had my "Scrollback lines" set to 1000. Changed it to "Unlimited scrollback" and how life is good iTerm2:
By default RTL doesn't show comments, <script /> and <style /> tags. In my case I needed to test for a commented node in the DOM.
If you want your tests to include all the nodes, you can use prettyDOM like this:
// render DOM with a commented node
const html = {__html: '<!--this is a commented DOM element-->'};
const element = <div dangerouslySetInnerHTML={html} />;
const { container } = render(element);
// This is what tells RLT to print all nodes!
const prettyfiedDOM = prettyDOM(container, undefined, { filterNode: () => true}) || '';
expect(prettyfiedDOM.includes('<!--this is a commented DOM element-->')).toBeTruthy();
Notice that the filterNode function always returns true, which tells RTL to print all DOM nodes, and hence you can test comments, styles, tags, etc. You can read more on prettyDOM source code and configure your desired behavior of DOM filtering.
View the live demo here
Hope that helps!

Cordova backbutton breaking application

I have a problem with Cordova's android application based on Angular 5+. I've found that window.history.back() and similar native JS back functions make two problems:
when going back, a page is flashing. It seems like first, all HTML content loaded and after it CSS
In one page on a back action, my layout is broken (screens below)
Orginal view:
After back button:
What's curious - after changing phone orientation all backs to normal.
I've found a solution - instead of using vanilla JS back functions I've created mine using Angular Router:
I've subscribe on router's events and save all routes:
this._subs.push(this._router.events.subscribe((e) => {
if (e instanceof NavigationEnd) {
this._cordovaService.saveRoute(e.url);
}
}));
And if I want back, I use navigateByUrl function:
back(): void {
const lastRoute = this._routingHistory[this._routingHistory.length - 2];
if (lastRoute) {
this._router.navigateByUrl(lastRoute);
this._routingHistory.pop();
}
}
After implementing this functionality for my inApp back buttons all work fine - there is no flashing or breaking layout.
Although, after implemented this function for my physical back button, the error is the same - layout breaking or flashing. Below my implementation:
Service:
this.deviceReady = Observable.fromEvent(document, 'deviceready').publishReplay(1);
(this.deviceReady as ConnectableObservable<Event>).connect();
this.restore = Observable.fromEvent(document, 'resume').publishReplay();
(this.restore as ConnectableObservable<Event>).connect();
this.backbutton = Observable.fromEvent(document, 'backbutton').publishReplay();
(this.backbutton as ConnectableObservable<Event>).connect();
Using it:
this._subs.push(this._cordovaService.deviceReady.subscribe(
() => {
document.addEventListener('backbutton', function (e) {
e.preventDefault();
e.stopPropagation();
this._cordovaService.back();
}.bind(this), false);
}
)
);
I'm sure that function in backbutton is executed (I've logged some information) but the problem still occurs.
More information:
I'm using cordova version 8.0.0
I'm using the following plugins:
https://github.com/TheMattRay/cordova-plugin-wkwebviewxhrfix.git" />
Some hints:
Once, I've built Cordova's android applications which work great (with native JS back function) but after next build, all come back. In hockeyapp I see that in good working version lowest available Android version was 4.1. In new apps, it is 4.4.
I've tried to downgrade Cordova/android engine version but without any positive results.
Additionally, I want to work with the newest libraries available.
Thanks for any help in that case.
I've finally found the solution, based on the following blog's post: http://weblogs.thinktecture.com/thomas/2017/02/cordova-vs-zonejs-or-why-is-angulars-document-event-listener-not-in-a-zone.html, I've added below script just before cordova.js import:
<script>
(function () {
'use strict';
window.addEventListener = function () {
EventTarget.prototype.addEventListener.apply(this, arguments);
};
window.removeEventListener = function () {
EventTarget.prototype.removeEventListener.apply(this, arguments);
};
document.addEventListener = function () {
EventTarget.prototype.addEventListener.apply(this, arguments);
};
document.removeEventListener = function () {
EventTarget.prototype.removeEventListener.apply(this, arguments);
};
})();
</script>
<script type="text/javascript" src="cordova.js"></script>
More about why this error occurring you can read in this GitHub issue: https://github.com/angular/angular/issues/22509

Categories

Resources