31

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
  • 373
  • 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

52

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,492
  • 1
  • 13
  • 16
  • I am getting an error `unable to load lodash.debounce` – Prateek Surana Jun 05 '18 at 11:28
  • 1
    @PrateekSurana try `import { debounce } from 'lodash'` – Rafael Z. B. Bravo Jun 18 '18 at 17:35
  • 1
    Instead of using the HOC to wrap, I just tried to configure the onPress prop with a debounce function `onPress={debounce(this.props.action, 300, {leading: true, trailing: false)}` and it didn't work. I can still double press really quickly and my action function gets called twice. – milesmeow Feb 08 '19 at 01:39
  • debounce(200, this.addNames(names))} > not worked and I am getting an error as Expected a function – sejn Jun 24 '19 at 12:08
  • @Jes: Your function `addNames` must return a function to work as expected. `const addNames = names => () => { /* do something with "names" */ };` – Patrick Wozniak Jun 25 '19 at 04:44
  • My addNames is a method. Is there is any way to prevent this click more than one time – sejn Jun 26 '19 at 11:58
  • This causes problems with formik and relies on the backend responding faster than 300ms. A better solution is to disable the button on loading – Phil Jul 12 '22 at 17:23
14

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 again!!!!!!       
</Button>
danklad
  • 88
  • 8
Shin-00
  • 258
  • 2
  • 10
13

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
8

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 N
  • 6,198
  • 2
  • 47
  • 58
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
  • 139
  • 1
  • 7
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
  • 672
  • 9
  • 29
4

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
  • 527
  • 6
  • 12
  • 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;
             });

    };
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

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,222
  • 2
  • 17
  • 47
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,264
  • 1
  • 11
  • 15