ogie
ogie

Reputation: 1470

React Native Prevent Double Tap

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

Answers (12)

Axell Hernandez
Axell Hernandez

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

Sreeraj
Sreeraj

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

Sri ram kathirvel
Sri ram kathirvel

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

Rajesh N
Rajesh N

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

SudoPlz
SudoPlz

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

Batu
Batu

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

David Schumann
David Schumann

Reputation: 14813

After reading several github threads, SO articles and trying most solutions myself I have come to the following conclusions:


  1. Providing an additional key parameter to do "idempotent pushes" does not work consistently as of now. https://github.com/react-navigation/rfcs/issues/16

  2. 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.

  3. 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

Jeevan Takhar
Jeevan Takhar

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

goodhyun
goodhyun

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

Rahul Gaba
Rahul Gaba

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

swescot
swescot

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

Paul Dolphin
Paul Dolphin

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

Related Questions