gourav.singhal
gourav.singhal

Reputation: 413

Prevent Double tap in React native

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

Upvotes: 32

Views: 53050

Answers (11)

Shin-00
Shin-00

Reputation: 288

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>

Upvotes: 16

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

Littletime
Littletime

Reputation: 548

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.

Upvotes: 4

Peretz30
Peretz30

Reputation: 1314

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

Upvotes: 0

Rajesh N
Rajesh N

Reputation: 6693

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"
          />
    );
  }

Upvotes: 8

Andr&#233; Alencar
Andr&#233; Alencar

Reputation: 71

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

    };

Upvotes: 1

Prateek Surana
Prateek Surana

Reputation: 702

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.

Upvotes: 6

Metalliza
Metalliza

Reputation: 189

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.

Upvotes: 6

zino
zino

Reputation: 1472

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;

Upvotes: 0

Patrick Wozniak
Patrick Wozniak

Reputation: 1572

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" />

Upvotes: 53

Oleksandr Cherniavenko
Oleksandr Cherniavenko

Reputation: 1575

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

Upvotes: 13

Related Questions