Function firing twice on click in React component - javascript

TL/DR: My simple toggle function fires twice when button is clicked.
I'm using useEffect in a React (w/ Next.js) component so that I can target the :root <html> tag for which I need the class to be toggled. The code is the following:
useEffect(() => {
const toggleMode = () => {
const root = document.documentElement;
root.classList.toggle("dark");
console.log("click");
};
const toggleBtn = document.querySelector("#toggle-btn");
toggleBtn.addEventListener("click", toggleMode);
I have the necessary imports, the code is placed inside the main component function before the return, and there's no errors in the console at all.
The only issue is that the function is fired twice every time the button is clicked and I cannot find any reason why or solutions online.
Would really appreciate your help and please let me know if I'm missing any information.
Cheers!

Your problem is coming from registering the event listener in a non-react way.
By registering the listener via
const toggleBtn = document.querySelector("#toggle-btn");
toggleBtn.addEventListener("click", toggleMode);
you are setting up a new listener each time the function is run, even if the DOM is not updated. This could result in multiple listeners being registered and firing simultaneously.
You need to add your listener the react way.
function Component ( props ){
const [ isFirst, setIsFirst ] = useState( true );
const [ toggle, setToggle ] = useState( false );
useEffect(() => {
if( isFirst ) {
setIsFirst( false );
return;
}
document.documentElement.classList.toggle("dark");
}, [ toggle ] );
return <div>
<button id="toggle-btn" onClick = { e => setToggle( !toggle ) } />
</div>
}

I resolved a similar problem in this post: Why does my NextJS Code run multiple times, and how can this be avoided?
Your code should only run once if you disable react strict mode.

Related

keydown firing multiple times when keydown (reactjs)

I am adding an event listener and checking if its level 1, but when I press the space key once, it fires 50times or more. Please help
document.addEventListener("keyup", function(e) {
if(level === 1){
if(e.code === "Space") {
console.log('space press');
click1();
}
}
});
Since this is tagged with React, given the code you have here and the issue you describe, it is almost certain that you are binding an event listener every render. Which means you are ending up with way more listeners than you want. What you need to do is use React when you are using React.
For example below, we have an input that logs on any keypress, and we also manually create an event listener. At first, when you type, you will get one log for each. However, once you click the button (triggering a rerender), you will start getting multiple "manual" events, but still the single "react" event:
class Hello extends React.Component {
constructor(props) {
super(props);
this.state = { count: props.count };
}
inc() {
this.setState(prev => ({count: prev.count+1}));
}
render() {
document.addEventListener("keyup", function(e) {
console.log('manual space press');
});
return <div onKeyUp={(e) => {
console.log('React: space press');
}}>
<button onClick={() => this.inc()}>{this.state.count}</button>
<input />
</div>
}
}
ReactDOM.render(<Hello count={0}/>, document.getElementById('root'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id='root'></div>
This is something called Event Bubblingwhich basically means that the event gets fired once on each parent element until it reached HTML.
you can learn about it here: https://dev.to/eladtzemach/event-capturing-and-bubbling-in-react-2ffg#:~:text=Event%20Bubbling%20and%20Capturing%20in%20React&text=Bubbling%20is%20as%20straightforward%20as,our%20example%20in%20the%20beginning.
you are able to prevent the default behavior but it's generally a good practice to leave it as is if you don't have a specific use for disabling it.
from the code snippet, I don't see why is this tagged with react but another reason for your problem is that you may be putting this code inside of your render() function or inside of any react life cycle function which is causing this snippet to run with each rerender leaving you with a punch of unwanted listeners which is not only functionality you don't want but also something that will slow down you app
overtime ie. until the user refresh the page.
useEffect(() => {
const handleEscape = (event) => {
if (event.keyCode === 27) {
console.log('Hello')
}
};
window.addEventListener('keydown', handleEscape);
return () => {
window.removeEventListener('keydown', handleEscape);
};
}, []);

React : printing all past state when updating state

I'm curioused about the work of this code.
It's a simple code, I intended to raise counter for 1 and print on console.
but when I click the button the counter increases, also printing from 0 ~ to all the number that I increased.
run screenshot
Could you explain why this happens?
import { useState } from "react";
function App() {
const [counter, setCounter] = useState(0);
const onClick = () => {
window.addEventListener("click", () => {
console.log(counter);
});
setCounter((counter) => counter + 1);
};
return (
<div className="App">
<button onClick={() => onClick()}>Add & Print!</button>
<div>{counter}</div>
</div>
);
}
export default App;
You added onClick event to button tag and when the function is executed, onClick event to window is added. So whenever clicking button tag, addEventListener that order to add shows console.log is made. On the code, showing console.log doesn't need addEventListener because button tag already has onClick event.
In <button onClick={() => onClick()}>Add & Print!</button> tag, onClick event is executed whenever the tag is clicked,
Below,
window.addEventListener("click", () => {
console.log(counter);
});
addEventListener adds event.
So whenever clicking button tag, addEventListener is executed(that adds events).
import { useState } from "react";
function App() {
const [counter, setCounter] = useState(0);
const onClick = () => {
// doesn't need window.addEventListener
// window.addEventListener("click", () => {
console.log(counter);
// });
setCounter((counter) => counter + 1);
};
return (
<div className="App">
<button onClick={() => onClick()}>Add & Print!</button>
<div>{counter}</div>
</div>
);
}
export default App;
On every click you are adding an event listener. So on first click there is one event listener, on second 2, on third three and so on. (on nth click, n event listeners are there on window object).
There is also the situation of the event listener using stale state. The window event listener has closed over(closures) the old value of state and is logging it. So the first event listener is always using count = 0. The third event listener is using count = 2. So, on.

React Hooks: useEffect for modal event listener

I have a modal dialog that I want to close if the user clicks outside of the modal. I have written the following useEffect code but I run into following issue:
The modal dialog contains a number of children (React Nodes) and those children might change (e.g. the user deletes an entry of a list). Those interactions trigger my onClick method but as the clicked list item has been removed from the modal, the modal closes even though the click was within the modal.
I thought adding [ children ] at the second parameter for useEffect would cleanup the old effect event listener quick enough that the method does not run again but this is not the case.
I handled the same issue in a class component with a ignoreNextClick-state but there must be a cleaner solution, right?
useEffect( () => {
const onClick = ( event ) => {
const menu = document.getElementById( 'singleton-modal' );
if ( !menu ) return;
// do not close menu if user clicked inside
const targetInMenu = menu.contains( event.target );
const targetIsMenu = menu === event.target;
if ( targetInMenu || targetIsMenu ) return;
onCloseModal();
};
window.addEventListener( 'click', onClick, false );
return () => window.removeEventListener( 'click', onClick, false );
}, [ children ] );
I found a solution that does not require any sort of storing old props.
The useEffect call looks like this:
useEffect( () => {
const onClickOutside = () => onCloseModal();
window.addEventListener( 'click', onClickOutside, false );
return () => window.removeEventListener( 'click', onClickOutside );
}, [] );
Adding the following click listener to the modal directly will stop the window click-listener from being called if the user clicked inside the modal.
<div
className={`modal ${ classes }`}
onClick={event => event.stopPropagation()}
role="presentation"
>
{children}
</div>`
I also added the role presentation to make the modal more accessible and aria-conform.
You can check parent of modal from the event.target.
If the current target is within the modal then return.
You can use closest to do that.
See the following solution.
...
if (event.target.closest( '.singleton-modal' ) || event.target.classList.contains('singleton-modal')) {
return;
}
...

Click event inside of Modal: Cannot read property 'click' of null

Trying to figure out why my click test event is not working. I applied the same setup for another click event on the same file and it worked.
Using Jest and Enzyme for react js
Goal : click event able to capture the node and test to pass
This is what i have for my test case so far:
Test.js
describe("Test Modal Components and Events ", () => {
let mountingDiv;
let wrapper;
beforeEach(() => {
wrapper = mount(<MemoryRouter keyLength={0} initialEntries={["/add"]} ><Policies {...baseProps} /></MemoryRouter>);
mountingDiv = document.createElement('div');
document.body.appendChild(mountingDiv);
})
test case
it('Test click event on Close - Modal', () => {
ReactModal.setAppElement('body');
wrapper = mount( <ReactModal isOpen></ReactModal>,
{attachTo: mountingDiv}
);
wrapper.setState({
quickFilterModalOpen: false,
})
wrapper.update()
expect(!!document.body.querySelector('.fullmodal')).toEqual(true);
expect(!!document.body.querySelector('.sidemodal_addnew_x')).toEqual(true)
document.querySelector("#closemodal-id").click();
});
Here is file.js
<Modal isOpen={this.state.quickFilterModalOpen} style={descriptionModalStyle}>
<div>
<div className='fullmodal'>
<div className='sidemodal_addnew_x' id="closemodal-id" onClick={this.closeModal}>
modal should be visible for the close button to be clicked. Set quickFilterModalOpen to true and use .find(selector) to find the element to be clicked. (https://airbnb.io/enzyme/docs/api/ReactWrapper/find.html)
wrapper.setState({
quickFilterModalOpen: true,
})
wrapper.update()
wrapper.find("#closemodal-id").simulate("click");
I think that is a binding issue, i will go for the event listener or otherwise go for an approach like
onClick = {this.closeModal.bind(this)} or similar, it depends on your code
Hope it helps

Simulate click event on react element

The bounty expires in 7 days. Answers to this question are eligible for a +50 reputation bounty.
ajaykools wants to reward an existing answer:
Worth bounty, only way simulate clicks on dynamic elements like svg, g, circle, etc which are generated on page load.
I'm trying to simulate a .click() event on a React element but I can't figure out why it is not working (It's not reacting when I'm firing the event).
I would like to post a Facebook comment using only JavaScript but I'm stuck at the first step (do a .click() on div[class="UFIInputContainer"] element).
My code is:
document.querySelector('div[class="UFIInputContainer"]').click();
And here's the URL where I'm trying to do it: https://www.facebook.com/plugins/feedback.php...
P.S. I'm not experienced with React and I don't know really if this is technically possible. It's possible?
EDIT: I'm trying to do this from Chrome DevTools Console.
React tracks the mousedown and mouseup events for detecting mouse clicks, instead of the click event like most everything else. So instead of calling the click method directly or dispatching the click event, you have to dispatch the down and up events. For good measure I'm also sending the click event but I think that's unnecessary for React:
const mouseClickEvents = ['mousedown', 'click', 'mouseup'];
function simulateMouseClick(element){
mouseClickEvents.forEach(mouseEventType =>
element.dispatchEvent(
new MouseEvent(mouseEventType, {
view: window,
bubbles: true,
cancelable: true,
buttons: 1
})
)
);
}
var element = document.querySelector('div[class="UFIInputContainer"]');
simulateMouseClick(element);
This answer was inspired by Selenium Webdriver code.
With react 16.8 I would do it like this :
const Example = () => {
const inputRef = React.useRef(null)
return (
<div ref={inputRef} onClick={()=> console.log('clicked')}>
hello
</div>
)
}
And simply call
inputRef.current.click()
Use refs to get the element in the callback function and trigger a click using click() function.
class Example extends React.Component{
simulateClick(e) {
e.click()
}
render(){
return <div className="UFIInputContainer"
ref={this.simulateClick} onClick={()=> console.log('clicked')}>
hello
</div>
}
}
ReactDOM.render(<Example/>, document.getElementById('app'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="app"></div>
If you don't define a class in your component, and instead you only declare:
function App() { ... }
In this case you only need to set up the useRef hook and use it to point/refer to any html element and then use the reference to trigger regular dom-events.
import React, { useRef } from 'react';
function App() {
const inputNameRef = useRef()
const buttonNameRef = useRef()
function handleKeyDown(event) {
// This function runs when typing within the input text,
// but will advance as desired only when Enter is pressed
if (event.key === 'Enter') {
// Here's exactly how you reference the button and trigger click() event,
// using ref "buttonNameRef", even manipulate innerHTML attribute
// (see the use of "current" property)
buttonNameRef.current.click()
buttonNameRef.current.innerHTML = ">>> I was forced to click!!"
}
}
function handleButtonClick() {
console.log('button click event triggered')
}
return (
<div>
<input ref={inputNameRef} type="text" onKeyDown={handleKeyDown} autoFocus />
<button ref={buttonNameRef} onClick={handleButtonClick}>
Click me</button>
</div>
)
}
export default App;
A slight adjustment to #carlin.scott's great answer which simulates a mousedown, mouseup and click, just as happens during a real mouse click (otherwise React doesn't detect it).
This answer adds a slight pause between the mousedown and mouseup events for extra realism, and puts the events in the correct order (click fires last). The pause makes it asynchronous, which may be undesirable (hence why I didn't just suggest an edit to #carlin.scott's answer).
async function simulateMouseClick(el) {
let opts = {view: window, bubbles: true, cancelable: true, buttons: 1};
el.dispatchEvent(new MouseEvent("mousedown", opts));
await new Promise(r => setTimeout(r, 50));
el.dispatchEvent(new MouseEvent("mouseup", opts));
el.dispatchEvent(new MouseEvent("click", opts));
}
Usage example:
let btn = document.querySelector("div[aria-label=start]");
await simulateMouseClick(btn);
console.log("The button has been clicked.");
Note that it may require page focus to work, so executing in console might not work unless you open the Rendering tab of Chrome DevTools and check the box to "emulate page focus while DevTools is open".
Inspired from previous solution and using some javascript code injection it is also possibile to first inject React into the page, and then to fire a click event on that page elements.
let injc=(src,cbk) => { let script = document.createElement('script');script.src = src;document.getElementsByTagName('head')[0].appendChild(script);script.onload=()=>cbk() }
injc("https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js",() => injc("https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js",() => {
class ReactInjected extends React.Component{
simulateClick(e) {
e.click()
}
render(){
return <div className="UFIInputContainer"
ref={this.simulateClick} onClick={()=> console.log('click injection')}>
hello
</div>
}
}
ReactDOM.render(<ReactInjected/>, document.getElementById('app'))
} ))
<div id="app"></div>
Kind of a dirty hack, but this one works well for me whereas previous suggestions from this post have failed. You'd have to find the element that has the onClick defined on it in the source code (I had to run the website on mobile mode for that). That element would have a __reactEventHandlerXXXXXXX prop allowing you to access the react events.
let elem = document.querySelector('YOUR SELECTOR');
//Grab mouseEvent by firing "click" which wouldn't work, but will give the event
let event;
likeBtn.onclick = e => {
event = Object.assign({}, e);
event.isTrusted = true; //This is key - React will terminate the event if !isTrusted
};
elem.click();
setTimeout(() => {
for (key in elem) {
if (key.startsWith("__reactEventHandlers")) {
elem[key].onClick(event);
}
}
}, 1000);
Using React useRef Hooks you can trigger a click event on any button like this:
export default const () => {
// Defining the ref constant variable
const inputRef = React.useRef(null);
// example use
const keyboardEvent = () => {
inputRef.current.handleClick(); //Trigger click
}
// registering the ref
return (
<div ref={inputRef} onClick={()=> console.log('clicked')}>
hello
</div>
)
}
This answer was inspired by carlin.scott code.
However, it works only with focusin event in my case.
const element = document.querySelector('element')
const events = ['mousedown', 'focusin']
events.forEach(eventType =>
element.dispatchEvent(
new MouseEvent(eventType, { bubbles: true })
)
)

Categories

Resources