28

How to prevent a user from tapping a button twice in React native?

i.e. A user must not be able tap twice quickly on a touchable highlight

gourav.singhal
  • 313
  • 1
  • 3
  • 7
  • Please add your code what you do have for click event. – Observer Nov 03 '17 at 19:12
  • There are some good suggestions on this already at [this stackoverflow question](https://stackoverflow.com/questions/36187081/react-native-prevent-double-tap) – kwishnu Nov 03 '17 at 19:50
  • 2
    Possible duplicate of [React Native Prevent Double Tap](https://stackoverflow.com/questions/36187081/react-native-prevent-double-tap) – Jeremy Nov 03 '17 at 20:11

11 Answers11

50

https://snack.expo.io/@patwoz/withpreventdoubleclick

Use this HOC to extend the touchable components like TouchableHighlight, Button ...

import debounce from 'lodash.debounce'; // 4.0.8

const withPreventDoubleClick = (WrappedComponent) => {

  class PreventDoubleClick extends React.PureComponent {

    debouncedOnPress = () => {
      this.props.onPress && this.props.onPress();
    }

    onPress = debounce(this.debouncedOnPress, 300, { leading: true, trailing: false });

    render() {
      return <WrappedComponent {...this.props} onPress={this.onPress} />;
    }
  }

  PreventDoubleClick.displayName = `withPreventDoubleClick(${WrappedComponent.displayName ||WrappedComponent.name})`
  return PreventDoubleClick;
}

Usage

import { Button } from 'react-native';
import withPreventDoubleClick from './withPreventDoubleClick';

const ButtonEx = withPreventDoubleClick(Button);

<ButtonEx onPress={this.onButtonClick} title="Click here" />
Patrick Wozniak
  • 1,365
  • 1
  • 11
  • 15
12

Use property Button.disabled

import React, { Component } from 'react';
import { AppRegistry, StyleSheet, View, Button } from 'react-native';

export default class App extends Component {
  
  state={
    disabled:false,
  }
  
  pressButton() {
    this.setState({
      disabled: true,
    });
    
    // enable after 5 second
    setTimeout(()=>{
       this.setState({
        disabled: false,
      });
    }, 5000)
  }
  
  render() {
    return (
        <Button
            onPress={() => this.pressButton()}
            title="Learn More"
            color="#841584"
            disabled={this.state.disabled}
            accessibilityLabel="Learn more about this purple button"
          />
    );
  }
}



// skip this line if using Create React Native App
AppRegistry.registerComponent('AwesomeProject', () => App);
  • This implementation doesn't handle default value for disabled prop. Passed from parent component. You should edit initial state. – Peretz30 Oct 29 '19 at 10:39
  • 1
    Also, this cause useless re-render, when you need just dont allow user click twice, but instead this you change state of button on UI – whalemare May 16 '20 at 11:24
6

I use it by refer the answer above. 'disabled' doesn't have to be a state.

import React, { Component } from 'react';
import { TouchableHighlight } from 'react-native';

class PreventDoubleTap extends Component {
    disabled = false;
    onPress = (...args) => {
        if(this.disabled) return;
        this.disabled = true;
        setTimeout(()=>{
            this.disabled = false;
        }, 500);
        this.props.onPress && this.props.onPress(...args);
    }
}

export class ButtonHighLight extends PreventDoubleTap {
    render() {
        return (
            <TouchableHighlight
                {...this.props}
                onPress={this.onPress}
                underlayColor="#f7f7f7"
            />
        );
    }
}

It can be other touchable component like TouchableOpacity.

Metalliza
  • 89
  • 1
  • 6
6

If you are using react navigation then use this format to navigate to another page. this.props.navigation.navigate({key:"any",routeName:"YourRoute",params:{param1:value,param2:value}})

The StackNavigator would prevent routes having same keys to be pushed in the stack again. You could write anything unique as the key and the params prop is optional if you want to pass parameters to another screen.

Prateek Surana
  • 597
  • 7
  • 28
5

Agree with Accepted answer but very simple way , we can use following way

import debounce from 'lodash/debounce';

    componentDidMount() {

       this.onPressMethod= debounce(this.onPressMethod.bind(this), 500);
  }

onPressMethod=()=> {
    //what you actually want on button press
}

 render() {
    return (
        <Button
            onPress={() => this.onPressMethod()}
            title="Your Button Name"
          />
    );
  }
Rajesh Nasit
  • 5,035
  • 2
  • 37
  • 50
5

Here is my simple hook.

import { useRef } from 'react';

const BOUNCE_RATE = 2000;

export const useDebounce = () => {
  const busy = useRef(false);

  const debounce = async (callback: Function) => {
    setTimeout(() => {
      busy.current = false;
    }, BOUNCE_RATE);

    if (!busy.current) {
      busy.current = true;
      callback();
    }
  };

  return { debounce };
};

This can be used anywhere you like. Even if it's not for buttons.

const { debounce } = useDebounce();

<Button onPress={() => debounce(onPressReload)}>
  Tap Me again and adain!
</Button>
Shin-00
  • 158
  • 1
  • 7
  • 1
    Worked perfectly in RN debouncing a button press, solutions with lodash did not work for me. – Jordan Grant Apr 25 '22 at 01:04
  • i have about 10 button in screen, with useRef for each button, does it's cause memory problem? – famfamfam Apr 29 '22 at 19:32
  • Only 10 refs don't cause memory problem. We have so many hooks and refs inside of React.js even without recognizing. Also, I guess we usually don't need to call useDebounce() 10 times. Just call it once and apply the same debounce method for all the 10 buttons. – Shin-00 Apr 30 '22 at 12:53
3

The accepted solution works great, but it makes it mandatory to wrap your whole component and to import lodash to achieve the desired behavior. I wrote a custom React hook that makes it possible to only wrap your callback:

useTimeBlockedCallback.js

import { useRef } from 'react'

export default (callback, timeBlocked = 1000) => {
  const isBlockedRef = useRef(false)
  const unblockTimeout = useRef(false)

  return (...callbackArgs) => {
    if (!isBlockedRef.current) {
      callback(...callbackArgs)
    }
    clearTimeout(unblockTimeout.current)
    unblockTimeout.current = setTimeout(() => isBlockedRef.current = false, timeBlocked)
    isBlockedRef.current = true
  }
}

Usage:

yourComponent.js

import React from 'react'
import { View, Text } from 'react-native'
import useTimeBlockedCallback from '../hooks/useTimeBlockedCallback'

export default () => {
  const callbackWithNoArgs = useTimeBlockedCallback(() => {
    console.log('Do stuff here, like opening a new scene for instance.')
  })
  const callbackWithArgs = useTimeBlockedCallback((text) => {
    console.log(text + ' will be logged once every 1000ms tops')
  })

  return (
    <View>
      <Text onPress={callbackWithNoArgs}>Touch me without double tap</Text>
      <Text onPress={() => callbackWithArgs('Hello world')}>Log hello world</Text>
    </View>
  )
}

The callback is blocked for 1000ms after being called by default, but you can change that with the hook's second parameter.

Littletime
  • 499
  • 6
  • 11
  • 1
    You might want to put everything in that return function inside of the if, otherwise it will reset the timer every time the button is pressed without calling the function again. – Chris Sandvik Sep 21 '20 at 18:07
1

I have a very simple solution using runAfterInteractions:

   _GoCategoria(_categoria,_tipo){

            if (loading === false){
                loading = true;
                this.props.navigation.navigate("Categoria", {categoria: _categoria, tipo: _tipo});
            }
             InteractionManager.runAfterInteractions(() => {
                loading = false;
             });

    };
0

You can also show a loading gif whilst you await some async operation. Just make sure to tag your onPress with async () => {} so it can be await'd.

import React from 'react';
import {View, Button, ActivityIndicator} from 'react-native';

class Btn extends React.Component {
    constructor(props) {
        super(props);

        this.state = {
            isLoading: false
        }
    }

    async setIsLoading(isLoading) {
        const p = new Promise((resolve) => {
            this.setState({isLoading}, resolve);
        });
        return p;
    }

    render() {
        const {onPress, ...p} = this.props;

        if (this.state.isLoading) {
            return <View style={{marginTop: 2, marginBottom: 2}}>
                <ActivityIndicator
                    size="large"
                />
            </View>;
        }


        return <Button
            {...p}
            onPress={async () => {
                await this.setIsLoading(true);
                await onPress();
                await this.setIsLoading(false);
            }}
        />
    }

}

export default Btn;
zino
  • 1,294
  • 2
  • 13
  • 31
0

My implementation of wrapper component.

import React, { useState, useEffect } from 'react';
import { TouchableHighlight } from 'react-native';

export default ButtonOneTap = ({ onPress, disabled, children, ...props }) => {
    const [isDisabled, toggleDisable] = useState(disabled);
    const [timerId, setTimerId] = useState(null);

    useEffect(() => {
        toggleDisable(disabled);
    },[disabled]);

    useEffect(() => {
        return () => {
            toggleDisable(disabled);
            clearTimeout(timerId);
        }
    })


    const handleOnPress = () => {
        toggleDisable(true);
        onPress();
        setTimerId(setTimeout(() => {
            toggleDisable(false)
        }, 1000))
    }
    return (
        <TouchableHighlight onPress={handleOnPress} {...props} disabled={isDisabled} >
            {children}
        </TouchableHighlight>
    )
}
Peretz30
  • 1,166
  • 1
  • 10
  • 15
0

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,119
  • 10
  • 28
  • 46