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
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" />
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);
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>
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"
/>
);
}
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.
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.
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.
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;
});
};
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.
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;
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>
)
}
Related
I want to test the modal component, but there is an error with defining the cancel button,
it renders only if it's not mobile.
isMobile is a variable that is a boolean value from hook - useBreakpoint (ant design library hook).
I don't know how to mock that value, or how to click that button.
Note: if I remove the isMobile check, the button clicks well:)
import React from 'react'
import {Grid, Typography} from 'antd'
import {Button} from '#/components/Button'
import {Modal} from '#/components/Modal'
import translations from './translations'
import {ConfirmationModalProps} from './props'
const {Text} = Typography
const {useBreakpoint} = Grid
export const ConfirmationModal = ({visible, onClose, children}: ConfirmationModalProps) => {
const screens = useBreakpoint()
const isMobile = screens.xs
return (
<Modal
title={translations().chargeConfirmation}
visible={visible}
onOk={onClose}
onCancel={onClose}
footer={[
!isMobile && (
<Button role={'cancel-button'} type={'ghost'} key={'cancel'} onClick={onClose}>
{ translations().cancel }
</Button>
),
<Button type={'primary'} key={'charge'} onClick={onClose}>
{ translations().confirm }
</Button>
]}
>
<Text>{translations().confirmationText(children)}</Text>
</Modal>
)
}
describe('ConfirmationModal', () => {
it('should should the children and close button', async () => {
const onClose = jest.fn()
jest.mock('antd/es/grid/hooks/useBreakpoint', () => ({
xs: false
}))
render(<ConfirmationModal onClose={onClose} visible={true}>100</ConfirmationModal>)
const child = screen.getByText('Are you sure you want to charge 100')
expect(child).toBeTruthy()
expect(screen.queryByTestId('cancel')).toBeDefined()
await waitFor(() => screen.queryByTestId('cancel'))
fireEvent.click(screen.queryByRole('cancel-button'))
expect(onClose).toHaveBeenCalledTimes(1)
})
})
Errors are:
Error: Unable to fire a "click" event - please provide a DOM element.
Unable to find an accessible element with the role "cancel-button"
Depending on queryByRole or getByRole selector.
What is wrong?
Let's take a look at the source code of the useBreakpoint hook.
import { useEffect, useRef } from 'react';
import useForceUpdate from '../../_util/hooks/useForceUpdate';
import type { ScreenMap } from '../../_util/responsiveObserve';
import ResponsiveObserve from '../../_util/responsiveObserve';
function useBreakpoint(refreshOnChange: boolean = true): ScreenMap {
const screensRef = useRef<ScreenMap>({});
const forceUpdate = useForceUpdate();
useEffect(() => {
const token = ResponsiveObserve.subscribe(supportScreens => {
screensRef.current = supportScreens;
if (refreshOnChange) {
forceUpdate();
}
});
return () => ResponsiveObserve.unsubscribe(token);
}, []);
return screensRef.current;
}
export default useBreakpoint;
It uses ResponsiveObserve.subscribe() to get the supportScreens, it calls ResponsiveObserve.register(), the .register() method use window.matchMedia() underly. jestjs use JSDOM(a DOM implementation) as its test environment, but JSDOM does not implement window.matchMedia() yet. So we need to mock it, see Mocking methods which are not implemented in JSDOM
E.g.
import { render } from '#testing-library/react';
import React from 'react';
import { Grid } from 'antd';
const { useBreakpoint } = Grid;
describe('72021761', () => {
test('should pass', () => {
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation(
(query) =>
({
addListener: (cb: (e: { matches: boolean }) => void) => {
cb({ matches: query === '(max-width: 575px)' });
},
removeListener: jest.fn(),
matches: query === '(max-width: 575px)',
} as any)
),
});
let screensVar;
function Demo() {
const screens = useBreakpoint();
screensVar = screens;
return <div />;
}
render(<Demo />);
expect(screensVar).toEqual({
xs: true,
sm: false,
md: false,
lg: false,
xl: false,
xxl: false,
});
});
});
I am learning react native, have been getting this error setState is not a function in react native
I searched a lot but nothing was helpful enough.
I have created this simplified code to show the issue
import React, { useState } from "react";
import { Text, View, Button } from "react-native";
const Test = ({ Test1 }) => {
return (
<Button
onPress={() => {
Test1.setState(true);
}}
/>
);
};
const Test1 = () => {
const [state, setState] = useState(false);
if (state) {
return <Text>Test Working</Text>;
} else {
return <Text>Test Not Working</Text>;
}
};
const App = () => {
return (
<View>
<Test Test1={Test1} />
</View>
);
};
export default App;
this is the error: TypeError: Test1.setState is not a function
Please help me fix this.
States can be transferred to other component only as props. You need to call the Test1 component from the App and the Test component from the Test1, then you can pass the props to the Test from Test1. By this you don't need to move the state to other component. you can not pass any component as props and access state or methods from there. You can try this code:
import React, { useState } from "react";
import { Text, View, Button } from "react-native";
const Test = ({ setState}) => {
return (
<Button
onPress={() => {
setState(true);
}}
/>
);
};
const Test1 = () => {
const [state, setState] = useState(false);
if (state) {
return <Text>Test Working</Text>;
} else {
return <Test setState={setState} />;
}
};
const App = () => {
return (
<View>
<Test1 />
</View>
);
};
export default App;
import React, { useState } from "react";
import { Text, View, Button } from "react-native";
const Test = ({ setState }) => {
return (
<Button
onPress={() => {
setState(true);
}}
);
};
const Test1 = ({state}) => {
if (state) {
return <Text>Test Working</Text>;
} else {
return <Text>Test Not Working</Text>;
}
};
const App = () => {
const [state, setState] = useState(false);
return (
<View>
<Test1 state={state} />
<Test setState={setState} />
</View>
);
};
export default App;
There are two problems here.
Your Test1 component is not being used at all
Hooks, and local functions in general, may not be called outside of the component that they are declared on
If you want to manage some local state in your Test component, it needs to live in that component.
I want to include the debounce function when the button is pressed.
I've written a debounce function in ChildComponent. It works in ChildComponent but I'm not getting the callback in ParentComponent.
Parent:
<MyButton onPress={() => alert("My Button clicked")} />
Child:
const MyButton = props => {
const {title = 'Enter', style = {}, textStyle = {}, onPress} = props;
const delayedOnPress = useCallback(
debounce(() => {
console.log(onPress);
return onPress;
}, 500),
[],
);
const onPressed = () => {
return delayedOnPress();
};
return (
<TouchableOpacity onPress={onPressed} style={[styles.button, style]}>
<Text style={[styles.text, textStyle]}>{title}</Text>
</TouchableOpacity>
);
};
Can anyone update me What I was doing wrong?
Thanks
Some notice points:
Use an arrow function inside useCallback.
No need to return anything inside debounce and handler function.
import React, { useCallback } from "react";
import "./styles.css";
import { TouchableOpacity, Text } from "react-native";
import { debounce } from "lodash";
const debounceAction = debounce(e => {
console.log(e); // "My Button clicked"
alert(e); // "My Button clicked"
}, 500);
const MyButton = props => {
const { onPress } = props;
const delayedOnPress = useCallback(e => debounceAction(e), []);
const onPressed = () => {
delayedOnPress(onPress);
};
return (
<TouchableOpacity onPress={onPressed}>
<Text>Button</Text>
</TouchableOpacity>
);
};
export default function App() {
return (
<div className="App">
<MyButton onPress="My Button clicked" />
</div>
);
}
Update
If we want to pass a customized function from the parent as a prop:
const debounceAction = debounce(e => {
e(); // customized function been passed
}, 500);
...
export default function App() {
const customizedFunction = () => { // customized function
alert("test");
};
return (
<div className="App">
<MyButton onPress={customizedFunction} /> // notice no `()` here
</div>
);
}
I created a custom toast component in my exercise React application. It is working correctly until the moment I try to introduce an auto dismiss timeout functionality. Basically when you load a new toast it needs to dismiss itself after let say 5000ms.
If you want check the full code in my Github Repo that also have a live preview.
Easiest way to create toast is put invalid mail / password.
I believe I am doing something wrong with the useEffect hook or I am missing something. The problem is that when I am creating multiple toasts they disappear all at the same time. Also React is complaining that I didn't include remove as a dependency of the useEffect hook but when I do it becomes even worse. Can someone demystify why this is happening and how it can be fixed. I am a bit new to React.
Here is the file that creates a HOC around my main App component:
import React, { useState } from 'react';
import { createPortal } from 'react-dom';
import ToastContext from './context';
import Toast from './Toast';
import styles from './styles.module.css';
function generateUEID() {
let first = (Math.random() * 46656) | 0;
let second = (Math.random() * 46656) | 0;
first = ('000' + first.toString(36)).slice(-3);
second = ('000' + second.toString(36)).slice(-3);
return first + second;
}
function withToastProvider(Component) {
function WithToastProvider(props) {
const [toasts, setToasts] = useState([]);
const add = (content, type = 'success') => {
const id = generateUEID();
if (toasts.length > 4) {
toasts.shift();
}
setToasts([...toasts, { id, content, type }]);
};
const remove = id => {
setToasts(toasts.filter(t => t.id !== id));
};
return (
<ToastContext.Provider value={{ add, remove, toasts }}>
<Component {...props} />
{ createPortal(
<div className={styles.toastsContainer}>
{ toasts.map(t => (
<Toast key={t.id} remove={() => remove(t.id)} type={t.type}>
{t.content}
</Toast>
)) }
</div>,
document.body
) }
</ToastContext.Provider>
);
}
return WithToastProvider;
}
export default withToastProvider;
And the Toast component:
import React, { useEffect } from 'react';
import styles from './styles.module.css';
function Toast({ children, remove, type }) {
useEffect(() => {
const duration = 5000;
const id = setTimeout(() => remove(), duration);
console.log(id);
return () => clearTimeout(id);
}, []);
return (
<div onClick={remove} className={styles[`${type}Toast`]}>
<div className={styles.text}>
<strong className={styles[type]}>{type === 'error' ? '[Error] ' : '[Success] '}</strong>
{ children }
</div>
<div>
<button className={styles.closeButton}>x</button>
</div>
</div>
);
}
export default Toast;
Searching today for the solution I found it here
You will need to use useRef and its current property
Here is how I transformed the Toast component to work:
import React, { useEffect, useRef } from 'react';
import styles from './styles.module.css';
function Toast({ children, remove, type }) {
const animationProps = useSpring({opacity: .9, from: {opacity: 0}});
const removeRef = useRef(remove);
removeRef.current = remove;
useEffect(() => {
const duration = 5000;
const id = setTimeout(() => removeRef.current(), duration);
return () => clearTimeout(id);
}, []);
return (
<div onClick={remove} className={styles[`${type}Toast`]}>
<div className={styles.text}>
<strong className={styles[type]}>{type === 'error' ? '[Error] ' : '[Success] '}</strong>
{ children }
</div>
<div>
<button className={styles.closeButton}>x</button>
</div>
</div>
);
}
export default Toast;
I'm trying to set up a HOC in React to able to apply text selection detection to any Input component. However I seem to be missing something when I was trying to put it together.
I was following this article here on how to create a HOC:
https://levelup.gitconnected.com/understanding-react-higher-order-components-by-example-95e8c47c8006
My code (before the article looked like this):
import { func } from 'prop-types';
import React, { PureComponent } from 'react';
import { Input } from 'reactstrap';
class SelectableInput extends PureComponent {
handleMouseUp = () => {
const selection = window.getSelection();
if (selection) {
this.props.onSelectionChanged(selection.toString());
}
};
render() {
// eslint-disable-next-line
const { onSelectionChanged, ...rest } = this.props;
return <Input onMouseUp={this.handleMouseUp} {...rest} />;
}
}
SelectableInput.propTypes = {
onSelectionChanged: func
};
export default SelectableInput;
And I was using it like this:
render() {
return (
<SelectableInput
type="textarea"
name="textarea-input"
value={'This is some txt'}
onSelectionChanged={onTextSelectionChanged}
id="textarea-input"
onChange={e => this.onPageDataChanged(e)}
dir="rtl"
rows="14"
placeholder="Placeholder..."
/>
);
}
After reading the article I changed the above code to:
const SelectableInput = WrappedInput => {
class SelectableInputHOC extends PureComponent {
handleMouseUp = () => {
const selection = window.getSelection();
if (selection) {
this.props.onSelectionChanged(selection.toString());
}
};
render() {
// eslint-disable-next-line
const { onSelectionChanged, ...rest } = this.props;
return <WrappedInput onMouseUp={this.handleMouseUp} {...rest} />;
}
}
SelectableInputHOC.propTypes = {
onSelectionChanged: func
};
};
export default SelectableInput;
My question is how do I actually go about using it now in a render() function?
Thank you for your advance for your help.
SelectableInput is a function that returns a function that takes a component as a parameter and returns another component. You can use it like this:
const ResultComponent = ({...props}) =>
SelectableInput({...props})(YourParamComponent);
Then render ResultComponent wherever you want.
Here you have an example of using a HOC and passing props to it:
https://jsfiddle.net/58c7tmx2/
HTML:
<div id="root"></div>
JS
const YourParamComponent = ({ name }) => <div>Name: {name}</div>
const SelectableInput = ({...props}) =>
WrappedInput => <WrappedInput {...props} />
const ResultComponent = ({...props}) =>
SelectableInput({...props})(YourParamComponent);
const App = () => <ResultComponent name="textarea-input" />
ReactDOM.render(
<App />,
document.getElementById('root')
)