How to stop JavaScript events when React component unmounts? - javascript

Background:
I have a web app built with React (v. 16.4.2 currently). It will only ever be used on a touch screen. It is composed of a ton of buttons to do things, and since it's all touch, I'm using touchstart/touchend to handle these actions.
Example:
This is a basic example of how I'm using the events. You click a button, it sets this.state.exampleRedirect to true, which in turn, makes the component re-render and then go to the new page (using react-router-dom). This is all working fine.
<button
type='button'
onTouchStart={() => this.setState({ exampleRedirect: true })}
className='o-button'>
Open modal
</button>
Issue:
I originally used onClick to handle buttons but had issues because my users have fat fingers and not a lot of tech background, and when they'd touch a button, they'd drag their finger over the button and it wouldn't fire the click. OnTouchStart fixes this problem by firing the minute any touch happens (drag, swipe, tap, etc).
The issue is with onTouchStart. A user touches the button, it quickly changes the page (using the router) and re-renders the new page. The app is fast, so this is almost instantaneous, which means that when the new page loads, the user's finger is usually still on the screen, thus firing ANOTHER touch event on whatever they're touching. This is often another routing button, so it just fires through screens until they lift their finger.
I am working around this by putting a delay on enabling buttons on each page load.
// example component
import React, { Component } from 'react';
class ExampleComponent extends Component {
state = { buttonsDisabled: true }
// after 300ms, the buttons are set to enabled (prevents touch events
// from firing when the page first loads
componentWillMount() {
timeoutId = setTimeout(() => {
this.setState({ buttonsDisabled: false });
}, 300);
}
render() {
return (
// button in render method
<button
disabled={this.state.buttonsDisabled}
type='button'
onTouchStart={() => this.setState({ exampleRedirect: true })}
className='o-button'>
Open modal
</button>
);
}
Is there a better way? Or a way to do what I'm doing, but globally so I don't have to add this jankity code in about 100 components?
Thanks!

Instead of using the onTouchStart event which is fired when a touch point is placed on the touch surface and using a timeout, which is a bit of a hack, you should make use of onTouchEnd since it will be fired when a touch point is removed from the touch surface, thereby ensuring that the mentioned case doesn't happen.
// example component
import React, { Component } from 'react';
class ExampleComponent extends Component {
state = { buttonsDisabled: true }
// after 300ms, the buttons are set to enabled (prevents touch events
// from firing when the page first loads
componentWillMount() {
timeoutId = setTimeout(() => {
this.setState({ buttonsDisabled: false });
}, 300);
}
render() {
return (
// button in render method
<button
disabled={this.state.buttonsDisabled}
type='button'
onTouchEnd={() => this.setState({ exampleRedirect: true })}
className='o-button'>
Open modal
</button>
);
}

Related

Material UI's ClickAwayListener firing right away, which causes state to toggle twice

I have been doing research on this for days and finally have decided to ask this on here. I am using react and material-ui's ClickAwayListener component. The idea is that I have a button, which toggles the this.state.showGridPopup to the opposite of whatever it is currently is. So if the user was to click it once, the grid popup should show, click it again, the popup should disappear. There is an handleShowGrid handler attached to the button that does this:
handleShowGrid = (event) => {
const { widgetButtonEl } = this.state;
const element = widgetButtonEl === null ? event.currentTarget : null;
console.log('In handleShowGrid!!!!!');
this.setState({
showWidget: !this.state.showWidget,
widgetButtonEl: element
});
}
All this works well. And toggles the popup to show when we click on the button attached to the handler.
<ButtonBase onClick={this.handleShowGrid}>Open Swap</ButtonBase>
The issue is when I add Material-UI's ClickAwayListener. The reason for adding this is to close the grid when a user clicks outside the grid. Here is the ClickAwayListener below:
<ClickAwayListener onClickAway={this.handleCloseWidget}>
<SurveyGrid />
</ClickAwayListener>
And the handleCloseWidget handler:
handleCloseGrid = (e) => {
console.log('In handleCloseWidget!!!!!');
this.setState({
showWidget: !this.state.showWidget,
widgetButtonEl: null
});
}
So now clicking outside grid is fine and closes the Grid. However, now, clicking on the button that should toggle the Grid being show (this.state.showWidget), causes the handleCloseGrid to fire. And then afterwards the handleShowGrid would fire. Is there anyway to not have the handleCloseGrid handler to fire? It seems like an issue with propagation and event bubbling. Have tried different things like e.stopPropagation() but to no avail.

Prevent tab change on PrimeVue TabView component

I am using the TabView component from PrimeVue (Vue 3), and I want to stop the tab change if any changes are made by the user, the problem is that I don't know how. I've already tried passing the event and using preventDefault and stopPropagation but seems that it doesn't work and click event is still happening.
The procedure should be:
If any changes are made, user press the tab and a dialog appears.
If user clicks 'No', I should prevent the tab change and stop the click event
Here is the demo of what I'm trying to archive, should be simple https://codesandbox.io/s/aged-wave-yzl1k?file=/src/App.vue:0-1753
If a flag is true I want to show a confirm dialog and prevent the tab change if user dismiss it.
The component that I'm using for the TabView: https://primefaces.org/primevue/showcase/#/tabview
Thanks in advance,
From the docs it looks like that internally the component will first switch tabs and then emit "tab-click", which explains the issue you're seeing. The exception is if the tab is disabled, in which case it won't change tabs but will emit "tab-click".
It took a bit to figure out, but there is a way to get the functionality you need with only a small adjustment. It requires a change in your main.js as well as in your App.vue file.
// main.js
/*
* Put this after you import TabView.
* This will prevent automatic tab switching but still emits
* the event to your application.
*/
TabView.methods.onTabClick = function(event, i) {
this.$emit('tab-click', {
originalEvent: event,
index: i
});
}
// App.vue
const onTabClick = (event) => {
if (changes.value) {
confirm.require({
message:
"Are you sure that you want to leave this tab? You'll lose your changes",
icon: "fal fa-exclamation-triangle",
acceptLabel: "Yes",
rejectIcon: "No",
accept: () => {
alert("here we should allow tab change");
activeIndex.value = event.index; // manually set activeIndex
},
reject: () => {
alert("stop tab change");
},
});
}
};
These changes modify what the onTabClick library method to only emit the event, without automatically switching. Then in your app you can check the index property of the event to determine what should be set to active.

How can I focus a button on click in ReactJS

I have the following problem, in my web site build with nextJS and ReactJS with typescript I have products that are loaded when a button is clicked, when I click the button the items appeared and the button is scrolled down, which is the asked behavior, but when I scroll to the bottom of the page and I try to click the button the scroll remains on the same position and the items are loaded but cannot be seen, my logic is to use onFocus on the current button and when I click it to change the scroll to him, that will solve the problem when the user has scrolled down to the bottom of the page, that way it will not remain on the bottom but rather it will automatically scroll up to the button and will see the new items loaded.
The problem is that the logic to load the products are in a different component in which I am reusing the current button and right prop I am sending function to the onClick on the button. My question is how can I use onFocus. Does it has to be in the child component inside the function or in the button component. I tried to make it work on the Button component, but it doesn't work. So I am stuck for the last 4 hours and I really need a push. I would be glad if you could shine some enlargement
Here I will enter the function in the parent component for the onClick prop :
const handleLoadMoreProducts = () => {
if (!isSearchPage) {
const mappedBreadcrumbs: string[] = categoryData.breadcrumbs.map(
(crumb: BreadCrumItem) => crumb.name
);
gtmBreadcrumbTrack(mappedBreadcrumbs);
}
<LoadMoreProducts handleLoadMoreProducts={handleLoadMoreProducts} />
And here is the component that uses the Button:
interface LoadMoreProductsProps {
handleLoadMoreProducts?: (MouseEvent: React.MouseEvent<HTMLButtonElement>) => void;
Focus?: (MouseEvent: React.MouseEvent<HTMLButtonElement>) => void;
}
const LoadMoreProducts: FC<LoadMoreProductsProps> = ({ handleLoadMoreProducts }) => (
<div className="LoadMoreProducts">
<Button type="button" importance="ghost" onClick={handleLoadMoreProducts}>
Load more products
</Button>
</div>
);
I think what you want to do is to forward the ref of the element you are trying to focus in the Button component using React.forwardRef and combine it with the useImperativeHandle hook in order to gain the ability to trigger the focus with the ref outside of the Button component.
You could create a ref for the element you are trying to focus and call the focus() function for the ref on click.
More information regarding forwarding refs and the useImperativeHandle hook.

How to use stopPropagation() with Next.js?

I am trying to use stopPropagation so my dropdown menu will close after I click on any other elements.
When I try to use stopPropagation & updating a boolean state the button re-render the page (which reset the state back to 'false') than updating the state to true and it stuck on "true" no matter what.
I wrote a code for an example:
import { useEffect, useState } from "react";
export default function IndexPage() {
const [state, setState] = useState(false);
useEffect(() => {
document.body.addEventListener("click", () => {
setState(false);
});
});
const onButtonClick = (e) => {
e.stopPropagation();
setState(!state);
};
console.log(state);
return (
<div>
<button onClick={onButtonClick}>Click</button>
<h1>{state ? "true" : "false"}</h1>
</div>
);
}
This problem seems to appear only with Next.js
I wrote the same code with React app and there is no problem.
Next.js codesandbox link
React codesandbox link
Edit:
When you fork the Next.js sandbox project everything is working fine. (and yet it doesn't solve the problem with my original code).
Anyone knows why is it happening?
This is a strange one. In the codesandbox container the state is getting preserved for overtime edits and not causing any re-renders as well so probably your e.stopPropagation when added to event handler didn't trigger a re-render and so the old handler for the button is still attached which means that the event listener on the body will keep on triggering as you click the button since our handler never got updated to factor in e.stopPropagation.
When I fork your Next.js setup, everything works fine. Because it's not an incremental change now. The container is rebuilt for me and doesn't preserve any past state and so no stale event handlers or anything.
The best way to test the difference between both setup is to see if the console logs any value when you remove the e.stopPropagation line from both. You will see that Next.js doesn't but the React one does. That means React's state even though preserved still triggered a render and so on new render the event handler that got created knows that :-
Hey, I don't have e.stopPropagation anymore so my
event will bubble up
Update - It seems that hitting Save after each change is necessary with Next.js container for a re-render. Don't rely on edits alone like in case of React container.
I did found a fix for this bug and the first person Who comment me solved this issue.
I add to my code a cleanup for my event listener and it seems to solve the problem.
It seems like cleanup inside useEffect is not just a good practice but a "must".
Because of the cleanup useEffect re-render the page twice and not three times and it prevent it from another render which cause the problem.
Big thanks for anyone tried to help.
The fixed code is:
export default function IndexPage() {
const [state, setState] = useState(false);
useEffect(() => {
window.addEventListener("click", () => setState(false));
return () => window.removeEventListener("click", () => setState(false));
});
const onButtonClick = (e) => {
e.stopPropagation();
setState(!state);
};
console.log(state);
return (
<div>
<button onClick={onButtonClick}>Click</button>
<h1>{state ? "true" : "false"}</h1>
</div>
);
}
Next.js sandbox link

react-router stops routing after rerender from a concurrent event

I have a dropdown that is controlled via state.
Clicking on a button toggles it on. Clicking outside toggles it off.
The dropdown contains Links within my application, however, when the dropdown is being toggled off, route transition is prevented.
If autohide is disabled, routing works fine, however, it is desired to also hide the dropdown on route transition.
Please explain to me what is going on
Also please help me fix it
class App extends React.Component {
state = {
isNavShown: false
}
showNav = () => this.setState({isNavShown: true})
hideNav = event => {
// ... some more logic ...
// don't hide if autoHide is disabled
if (autoHide.checked === false) return
this.setState({isNavShown: false})
}
componentDidMount() {
document.addEventListener('mousedown', this.hideNav)
}
// ...
}
I have also tried wrapping the setState in setTimeout, but to no avail.
Here is the full jsfiddle https://jsfiddle.net/nimareq/1kh47uey/
So the issue is that your hideNav function is hiding the nav if the user clicks anywhere outside of show navigation button and the checkbox you built. However, if the user clicks on the nav itself it will be hidden before you have a chance to navigate the user.
Essentially, the browser will detect the click event listener you made on the document before it bubbles down to the anchor tag click. By the time it gets there the anchor tag is gone. (I hope that makes sense lol)
Anyways you can easily solve it by adding the following to your hideNav function:
if(nav.contains(event.target)) return;
Also don't forget to add the id="nav" on your navbar or whatever else you want to call it. This way the navbar won't disappear when u click on the navbar. It will still disappear if you click off the navbar.

Categories