20

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.

SherylHohman
  • 16,580
  • 17
  • 88
  • 94
efru
  • 1,401
  • 3
  • 17
  • 20

12 Answers12

8

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.

SherylHohman
  • 16,580
  • 17
  • 88
  • 94
SudoPlz
  • 20,996
  • 12
  • 82
  • 123
  • By doing this way, i couldn't pass params to the function like this `onPress={()=>this.onPressThrottledCb(data)}` – TomSawyer Jul 27 '17 at 19:14
  • lodash or underscore's implementation of throttle pass all params to the function automatically – mvandillen Dec 19 '17 at 13:07
  • Now using underscore, and the params do get passed. – SudoPlz Dec 19 '17 at 17:13
  • @Maximtoyberman what do you mean it doesn't work? Do you get any errors? Can you debug to see if your callback is being invoked? – SudoPlz Jan 15 '18 at 15:22
  • is using `this.onPressThrottledCb.bind(this)` instead of `this.onPressThrottledCb` still necessary if the method is defined as a class property? i.e. `onPressThrottledCb = () => {...}` – David Schumann Apr 13 '18 at 11:40
4

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);
David Schumann
  • 13,380
  • 9
  • 75
  • 96
Paul Dolphin
  • 758
  • 2
  • 8
  • 18
  • 2
    If you use the same screen twice this will fail. For example a quiz app. – Oliver Dixon Sep 08 '17 at 10:01
  • 1
    Yes, as the answer states it works by preventing going to the same route twice. If this is not suitable then use a different method. In most cases it is suitable – Paul Dolphin Sep 11 '17 at 09:12
  • In my previous project it was working fine but now it creates a warning: undefined is not an object (evaluating 'routeConfigs[initialRouteName].params'). How do I solve this? – Diptesh Atha Sep 06 '19 at 13:54
2

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.

David Schumann
  • 13,380
  • 9
  • 75
  • 96
swescot
  • 413
  • 1
  • 4
  • 10
2

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>
        );
    }
SherylHohman
  • 16,580
  • 17
  • 88
  • 94
Batu
  • 304
  • 2
  • 12
1

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});
David Schumann
  • 13,380
  • 9
  • 75
  • 96
goodhyun
  • 4,814
  • 3
  • 33
  • 25
1

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

Jeevan Takhar
  • 491
  • 5
  • 10
1

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.

David Schumann
  • 13,380
  • 9
  • 75
  • 96
1

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 >
Rajesh N
  • 6,198
  • 2
  • 47
  • 58
1

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,

Reegan Miranda
  • 2,879
  • 6
  • 43
  • 55
1

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>
)
}
Sreeraj
  • 2,690
  • 6
  • 26
  • 37
1

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.

Matt Ke
  • 3,599
  • 12
  • 30
  • 49
0

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

David Schumann
  • 13,380
  • 9
  • 75
  • 96
Rahul Gaba
  • 460
  • 4
  • 6