Reputation: 1470
I have a TouchableHighlight
wrapping a Text
block that when tapped, opens a new scene (which I'm using react-native-router-flux).
It's all working fine, except for the fact that if you rapidly tap on the TouchableHighlight
, the scene can render twice.
I'd like to prevent the user from rapidly being able to tap that button.
What is the best way to accomplish this in Native? I looked into the Gesture Responder System, but there aren't any examples or anything of the sort, which if you're new, like me, is confusing.
Upvotes: 20
Views: 20144
Reputation: 21
Did not use disable feature, setTimeout, or installed extra stuff.
This way code is executed without delays. I did not avoid double taps but I assured code to run just once.
I used the returned object from TouchableOpacity described in the docs https://reactnative.dev/docs/pressevent and a state variable to manage timestamps. lastTime is a state variable initialized at 0.
const [lastTime, setLastTime] = useState(0);
...
<TouchableOpacity onPress={async (obj) =>{
try{
console.log('Last time: ', obj.nativeEvent.timestamp);
if ((obj.nativeEvent.timestamp-lastTime)>1500){
console.log('First time: ',obj.nativeEvent.timestamp);
setLastTime(obj.nativeEvent.timestamp);
//your code
SplashScreen.show();
await dispatch(getDetails(item.device));
await dispatch(getTravels(item.device));
navigation.navigate("Tab");
//end of code
}
else{
return;
}
}catch(e){
console.log(e);
}
}}>
I am using an async function to handle dispatches that are actually fetching data, in the end I'm basically navigating to other screen.
Im printing out first and last time between touches. I choose there to exist at least 1500 ms of difference between them, and avoid any parasite double tap.
Upvotes: 1
Reputation: 2720
This worked for me as a workaround
import React from 'react';
import {TouchableOpacity } from 'react-native';
export default SingleClickTouchableOpacity = (props) => {
let buttonClicked = false
return(
<TouchableOpacity {...props} onPress={() => {
if(buttonClicked){
return
}
props.onPress();
buttonClicked = true
setTimeout(() => {
buttonClicked = false
}, 1000);
}}>
{props.children}
</TouchableOpacity>
)
}
Upvotes: 1
Reputation: 71
I fixed using this lodash method below,
Step 1
import { debounce } from 'lodash';
Step 2
Put this code inside the constructor
this.loginClick = debounce(this.loginClick .bind(this), 1000, {
leading: true,
trailing: false,
});
Step 3
Write on your onPress button like this
onPress={() => this.props.skipDebounce ? this.props.loginClick : this.loginClick ()}
Thanks,
Upvotes: 1
Reputation: 6693
You can do using debounce very simple way
import debounce from 'lodash/debounce';
componentDidMount() {
this.yourMethod= debounce(this.yourMethod.bind(this), 500);
}
yourMethod=()=> {
//what you actually want your TouchableHighlight to do
}
<TouchableHighlight onPress={this.yourMethod}>
...
</TouchableHighlight >
Upvotes: 1
Reputation: 23333
What you're trying to do is you want to limit your on tap callbacks, so that they will only run ONCE.
This is called throttling, and you can use underscore
for that:
Here's how:
_.throttle(
this.thisWillRunOnce.bind(this),
200, // no new clicks within 200ms time window
);
Here's how my react component looks after all.
class MyComponent extends React.Component {
constructor(props) {
super(props);
_.throttle(
this.onPressThrottledCb.bind(this),
200, // no new clicks within 200ms time window
);
}
onPressThrottledCb() {
if (this.props.onPress) {
this.props.onPress(); // this only runs once per 200 milliseconds
}
}
render() {
return (
<View>
<TouchableOpacity onPress={this.onPressThrottledCb}>
</TouchableOpacity>
</View>
)
}
}
I hope this helps you. In case you wanna learn more check this thread.
Upvotes: 8
Reputation: 308
I do it like this:
link(link) {
if(!this.state.disabled) {
this.setState({disabled: true});
// go link operation
this.setState({disabled: false});
}
}
render() {
return (
<TouchableHighlight onPress={() => this.link('linkName')}>
<Text>Go link</Text>
</TouchableHighlight>
);
}
Upvotes: 2
Reputation: 14813
After reading several github threads, SO articles and trying most solutions myself I have come to the following conclusions:
Providing an additional key
parameter to do "idempotent pushes" does not work consistently as of now.
https://github.com/react-navigation/rfcs/issues/16
Using debounce
slows down the UX significantly. The navigation only happens X ms after the user has pushed the button the last time. X needs to be large enough to bridge the time where double taps might happen. Which might be anything from 100-600ms really.
Using _.throttle
did not work for me. It saved the throttled function call and executed it after the timer ran out resulting in a delayed double tap.
I considered moving to react-native-navigation
but apparently the issue lies deeper and they experience it too.
So for now I built my own hack that interferes with my code the least:
const preventDoubleTapHack = (component: any, doFunc: Function) => {
if (!component.wasClickedYet__ULJyRdAvrHZvRrT7) {
// eslint-disable-next-line no-param-reassign
component.wasClickedYet__ULJyRdAvrHZvRrT7 = true;
setTimeout(() => {
// eslint-disable-next-line no-param-reassign
component.wasClickedYet__ULJyRdAvrHZvRrT7 = false;
}, 700);
doFunc();
}
};
anywhere, where we navigate instead of
this.props.navigation.navigate('MyRoute');
do
preventDoubleTapHack(this, () => this.props.navigation.navigate('MyRoute');
Beautiful.
Upvotes: 1
Reputation: 491
If you are using react-navigation
, you can supply a key
property to navigate
to ensure only one instance is ever added to the stack.
via https://github.com/react-navigation/react-navigation/issues/271
Upvotes: 1
Reputation: 5002
You could bounce the click at the actual receiver methods, especially if you are dealing with the state for visual effects.
_onRefresh() {
if (this.state.refreshing)
return
this.setState({refreshing: true});
Upvotes: 1
Reputation: 480
I fixed this bug by creating a module which calls a function only once in the passed interval.
Example: If you wish to navigate from Home -> About And you press the About button twice in say 400 ms.
navigateToAbout = () => dispatch(NavigationActions.navigate({routeName: 'About'}))
const pressHandler = callOnce(navigateToAbout,400);
<TouchableOpacity onPress={pressHandler}>
...
</TouchableOpacity>
The module will take care that it calls navigateToAbout only once in 400 ms.
Here is the link to the NPM module: https://www.npmjs.com/package/call-once-in-interval
Upvotes: 0
Reputation: 413
Perhaps you could use the new disable-feature introduced for touchable elements in 0.22? I'm thinking something like this:
Component
<TouchableHighlight ref = {component => this._touchable = component}
onPress={() => this.yourMethod()}/>
Method
yourMethod() {
var touchable = this._touchable;
touchable.disabled = {true};
//what you actually want your TouchableHighlight to do
}
I haven't tried it myself. So I'm not sure if it works.
Upvotes: 2
Reputation: 798
The following works by preventing routing to the same route twice:
import { StackNavigator, NavigationActions } from 'react-navigation';
const App = StackNavigator({
Home: { screen: HomeScreen },
Details: { screen: DetailsScreen },
});
// Prevents double taps navigating twice
const navigateOnce = (getStateForAction) => (action, state) => {
const { type, routeName } = action;
return (
state &&
type === NavigationActions.NAVIGATE &&
routeName === state.routes[state.routes.length - 1].routeName
) ? state : getStateForAction(action, state);
};
App.router.getStateForAction = navigateOnce(App.router.getStateForAction);
Upvotes: 4