Mock antd useBreakpoint hook - javascript

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

Related

React Unit testing onClick event

I have a Button wrapper component in which I am using mui button. I want to do the unit testing for this button wrapper component. I wrote some code but for onClick test it is falling.
index.tsx (ButtonWrapper Component)
import React from 'react';
import { Button } from '#material-ui/core';
import { ButtonProps } from '../../../model';
type ConfigButtonProps ={
// variant?: string,
// color?: string,
// fullWidth?: boolean,
// type?: string
}
export const ButtonWrapper = (props: ButtonProps) => {
const {children, onSubmit, disabled, type, onClick, ...otherprops} = props
console.log("button", otherprops, type);
const configButton:ConfigButtonProps = {
variant: 'contained',
color: 'primary',
fullWidth: true,
type: type
}
return (
<Button disabled={disabled} onClick={onSubmit} {...configButton}>
{children}
</Button>
);
};
index.test.tsx (Button Wrapper test)
import { ButtonWrapper } from "./index"
import { fireEvent, render, screen } from "#testing-library/react";
import { ButtonProps } from "../../../model";
const makeSut = (props: ButtonProps) => {
return render(<ButtonWrapper onClick={jest.fn()} {...props} />);
};
describe("<ButtonWrapper />", () => {
test("Button renders correctly",()=>{
render(<ButtonWrapper />)
const buttonElem = screen.getByRole('button')
expect(buttonElem).toBeInTheDocument()
})
test("Should call onClick successfully", () => {
const spy = jest.fn();
const { getByRole } = makeSut({ onClick: spy });
fireEvent.click(getByRole('button'));
expect(spy).toHaveBeenCalled();
});
});
FormContainer.tsx (Parent Container)
return (<form onSubmit={event=>this.onSubmit(event)}>
{/* <div>FormContainer {JSON.stringify(this.props.states, null, 2)}</div> */}
{this.state.fields.map((field,index)=>{
return <FormControl
key={field.id}
fieldConfig={field}
focused={(event:React.ChangeEvent<HTMLInputElement>)=>this.fieldBlur(event,field,index)}
changed={(event:React.ChangeEvent<HTMLInputElement>)=>this.fieldChange(event,field,index)} />
})}
<ButtonWrapper type='submit'>Submit</ButtonWrapper>
</form>)
Error
I also want to know in order to make 90% test coverage what else I need to test ?
[![enter image description here]]
I tried this below mentioned code also but the last line fails.
test("Should call onClick successfully", () => {
const onSubmitHandler = jest.fn();
render(<ButtonWrapper onClick={onSubmitHandler} />)
const buttonElement = screen.getByRole('button');
user.click(buttonElement)
expect(onSubmitHandler).toHaveBeenCalledTimes(1) //This line fails
});
Your spy function is coming in onClick, not the onSubmit prop:
const { getByRole } = makeSut({ onClick: spy });
And your component assigns the value from onSubmit to onClick:
<Button disabled={disabled} onClick={onSubmit} {...configButton}>
{children}
</Button>
About the coverage - I cannot see your coverage report but what I can see in your component - there is a disabled prop. You should render the component with true and false values for this prop. This should give you higher coverage.

Testing a component using react-intersection-observer

I have a sticky bar which appears at the bottom of the screen, but when the user scrolls to a particular element, in this case Apply Now button, then the sticky bar disappears (in scss I've set it to display: none;). I need to write test case for such behaviour, can someone please help me with this, been stuck to this since a long time.
My React component:
/* eslint-disable #typescript-eslint/no-unsafe-assignment,#typescript-eslint/no-unsafe-member-access */
import { ButtonActionEmphasis } from '#honeycomb-npm/honeycomb-react';
import { useEffect, useState } from 'react';;
import './ApplyNowStickyBar.scss';
const ApplyNowStickyBar = (): JSX.Element => {
const [isElementVisible, setIsElementVisible] = useState(false);
const applyNowButton = document.getElementById('calc-applyNowBtn');
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
const entry = entries[0];
setIsElementVisible(entry.isIntersecting);
})
if(applyNowButton !== null){
observer.observe(applyNowButton);
}
}, [applyNowButton]);
return (
<div data-testid='applyNowStickySection' className={isElementVisible ? 'apply-now-bar' : 'apply-now-sticky-bar'}>
<div className="section">
<ButtonActionEmphasis>
Apply Now
</ButtonActionEmphasis>
</div>
</div>
);
};
export default ApplyNowStickyBar;
I'm trying to do this in my test case but it always keeps failing and says that the element is visible
/* eslint-disable #typescript-eslint/no-unsafe-call */
/* eslint-disable #typescript-eslint/no-unsafe-member-access */
/* eslint-disable #typescript-eslint/no-unsafe-assignment */
import { fireEvent, render, RenderResult, screen, waitFor } from '#testing-library/react';
import { Provider } from 'react-redux';
import { store } from '../../store/store';
import ApplyNowStickyBar from './ApplyNowStickyBar';
const setup = (): RenderResult => {
const applyNowStickyBar = document.getElementById('calc-applyNowBtn');
const intersectionObserverMock = () => ({
observe: () => applyNowStickyBar
})
window.IntersectionObserver = jest.fn().mockImplementation(intersectionObserverMock);
return render(<Provider store={store}><ApplyNowStickyBar/></Provider>);
};
describe('ApplyNowStickyBar', () => {
it('should render', async () => {
const { baseElement } = setup();
await waitFor(() => expect(baseElement).toBeInTheDocument());
});
it('should not show apply now sticky bar when ', async () => {
Object.defineProperty(window, 'innerWidth', {writable: true, configurable: true, value: 300})
setup();
const applyNowBtn = document.getElementById('calc-applyNowBtn');
await fireEvent.scroll(window, { target: { scrollY: applyNowBtn ?.getBoundingClientRect().y } });
expect(screen.getByTestId('applyNowStickySection')).not.toBeVisible();
});
});

How to test if button was clicked?

I'm working with React Testing Library. The problem I have is that fireEvent doesn't trigger, and the error throws, is following:
Expected number of calls: >= 1
Received number of calls: 0
For this particular test do I need to work with #testing-library/react-hooks? Do I need to render my Product component wrapped on context provider where the function is imported from?
Product.tsx component
import React, { FunctionComponent } from "react";
import { useProducts } from "./ContextWrapper";
import { Button } from "#mui/material";
import { articleProps } from "./types";
const Product: FunctionComponent<{ article: articleProps }> = ({
article,
}) => {
const { handleAddToCart } = useProducts(); // this is from global context
return (
<Button
aria-label="AddToCart"
onClick={() => handleAddToCart(article)}
>
Add to cart
</Button>
);
};
export default Product;
Product.test.tsx
import React from "react";
import { fireEvent, render, screen } from "#testing-library/react";
import Product from "./Product";
import { act, renderHook } from "#testing-library/react-hooks";
const article = {
name: "samsung",
variantName: "phone",
categories: [],
};
const renderProduct = () => {
return render(
<ContextProvider>
<Product article={article} />
</ContextProvider>
);
};
//first test just tests if button exists with provided content and works fine
test("renders product", () => {
render(<Product article={article} />);
const AddToCartbutton = screen.getByRole("button", { name: /AddToCart/i });
expect(AddToCartbutton).toHaveTextContent("Add to cart");
});
// this test throws the error described above
test("test addToCart button", () => {
renderProduct();
const onAddToCart = jest.fn();
const AddToCartbutton = screen.getByRole("button", { name: /AddToCart/i });
fireEvent.click(AddToCartbutton);
expect(onAddToCart).toHaveBeenCalled();
});
ContextWrapper.tsx
import React, {createContext, useContext, ReactNode, useState} from "react";
import { prodProps } from "../types";
type ProductContextProps = {
productData: prodProps;
handleAddToCart: (clickedItem: productPropsWithAmount) => void;
};
const ProductContextDefaultValues: ProductContextProps = {
productData: null as any,
handleAddToCart:null as any;
};
type Props = {
children: ReactNode;
};
const ProductContext = createContext<ProductContextProps>(
ProductContextDefaultValues
);
export function useProducts() {
return useContext(ProductContext);
}
const ContextWrapper = ({ children }: Props) => {
const { data } = useGraphQlData();
const [productData, setProductData] = useState<Category>(data);
const [cartItems, setCartItems] = useState<prodProps[]>([]);
useEffect(() => {
if (data) {
setProductData(data);
}
}, [data]);
const handleAddToCart = (clickedItem: prodProps) => {
setCartItems((prev: prodProps[]) => {
return [...prev, { ...clickedItem }];
});
};
return (
<ProductContext.Provider
value={{
productData,
handleAddToCart,
}}
>
{children}
</ProductContext.Provider>
);
};
export default ContextWrapper;
I advise you to create Button Component and test it separately like this:
const onClickButton = jest.fn();
await render(<Button onClick={onClickButton} />);
const AddToCartbutton = screen.getByRole("button", { name: /AddToCart/i });
await fireEvent.click(AddToCartbutton);
expect(onClickButton).toHaveBeenCalled();
One way to accomplish this is to mock ContextWrapper, as your test is specifically referring to Product component.
So, you could do something like this into your test:
import * as ContextWrapper from '--- PATH TO ContextWrapper ---';
test('test addToCart button', () => {
/// mockFunction
const onAddToCart = jest.fn();
jest.spyOn(ContextWrapper, 'useProducts').mockImplementationOnce(() => ({
productData: {
example_prodProp1: 'initialValue1',
example_prodProp2: 'initialValue2',
},
// Set mockFunction to handleAddToCart of useProducts
handleAddToCart: onAddToCart,
}));
render(<Product article={article} />);
const AddToCartbutton = screen.getByRole('button', { name: /AddToCart/i });
fireEvent.click(AddToCartbutton);
expect(onAddToCart).toHaveBeenCalledWith({
prodProp1: 'samsung',
prodProp2: 'phone',
});
});
In this line jest.spyOn(ContextWrapper, 'useProducts').mockImplementation we are mocking useProducts return value and setting the handleAddToCart function to your mockFunction and that's how you can check if it has been called.
* This test is strictly focused on Product component and you just want to garantee that the component calls the handleAddToCart function from ContextWrapper.
For test how handleAddToCart should work, you can create a specific test for the ContextWrapper.

React test a component with saga

Hllo Guys, I'm having a bit trouble with testing my component
The problem is that I would like to test my React Native Component that uses saga to fetch data from server.
The Problem is that I do know what I'm supposed to do, I think I should mock my API calls in my test file but I do not know how :/
The component file is really simple, when mounted it dispatches action to fetch list on vehicles, and then it shows them in UI. And until that is fetched it shows loading text
Bellow are my current setup of components & test file.
Here is a screen component that fetches initial data on screen load
Screen Component
import React, { useContext, useEffect, useState } from 'react';
import { Platform, FlatList, View, ActivityIndicator, Text } from 'react-native';
import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux';
import { vehiclesActions } from '_store/vehicles';
export const MainScreen = ({ navigation }) => {
/**
* Redux selectors and dispatch
*/
const {
loading = true,
vehicles = [],
loadMore = false
} = useSelector((state) => state.vehicles);
/**
* Initial effect, fetches all vehicles
*/
useEffect(() => {
dispatch(
vehiclesActions.vehicleGet({
page: 1,
})
);
}, []);
const renderCard = () => {
return (<View><Text>Test</Text></View>)
}
if (loading) {
return (<View><Text>App Loading </Text></View>
}
return (
<View style={styles.wrapper}>
<View
style={
Platform.OS === 'ios' ? { marginTop: 30 } : { marginTop: 0, flex: 1 }
}
>
{!loading && (
<View style={Platform.OS === 'ios' ? {} : { flex: 1 }}>
<FlatList
testID={'flat-list'}
data={vehicles}
renderItem={renderCard}
/>
</View>
)}
</View>
</View>
);
};
MainScreen.propTypes = {
navigation: PropTypes.object
};
export default MainScreen;
My Vehicles Saga:
const api = {
vehicles: {
getVehicles: (page) => {
return api.get(`/vehicles/list?page=${page}`, {});
},
}
function* getVehicles(action) {
try {
const { page } = action.payload;
const { data } = yield call(api.vehicles.getVehicles, page);
yield put({ type: vehiclesConstants.VEHICLE_GET_SUCCESS, payload: data });
} catch (err) {
yield call(errorHandler, err);
yield put({ type: vehiclesConstants.VEHICLE_GET_FAIL });
}
}
export function* vehiclesSaga() {
yield takeLatest(vehiclesConstants.VEHICLE_GET_REQUEST, getVehicles);
}
Actions:
export const vehiclesActions = {
vehicleGet: payload => ({ type: vehiclesConstants.VEHICLE_GET_REQUEST, payload }),
vehicleGetSuccess: payload => ({ type: vehiclesConstants.VEHICLE_GET_SUCCESS, payload }),
vehicleGetFail: error => ({ type: vehiclesConstants.VEHICLE_GET_FAIL, error }),
}
Reducer
import { vehiclesConstants } from "./constants";
const initialState = {
vehicles: [],
loading: true,
};
export const vehiclesReducer = (state = initialState, action) => {
switch (action.type) {
case vehiclesConstants.VEHICLE_GET_REQUEST:
return {
...state,
loading: true,
};
case vehiclesConstants.VEHICLE_GET_SUCCESS:
return {
...state,
loading: false,
vehicles: action.payload,
};
}
}
My Test File
import 'react-native';
import React from 'react';
import {cleanup, render, fireEvent} from '#testing-library/react-native';
import AppScreen from '../../../../src/screens/App/index';
import {Provider} from 'react-redux';
import {store} from '../../../../src/store/configureStore';
describe('App List Component', () => {
beforeEach(() => jest.useFakeTimers());
afterEach(cleanup);
it('should render vehicle list page title', async () => {
const navigation = {
setParams: () => {},
navigate: jest.fn(),
};
const route = {
}
const component = (
<Provider store={store}>
<AppScreen route={route} navigation={navigation} />
</Provider>);
const {getByText, getByTestId} = render(component);
const pageTitle = await getByText('App Loading'); // this works fine
expect(pageTitle).toBeDefined();
});
it('should navigate to add vehicle', async () => {
const navigation = {
setParams: () => {},
navigate: jest.fn(),
};
const route = {
}
const component = (
<Provider store={store}>
<AppScreen route={route} navigation={navigation} />
</Provider>);
const {getByText, getByTestId} = render(component);
const flatList = await getByTestId('flat-list');// this throws error since flat list is still not shown, and loading is showing instead
});
Like I see above I cannot find element with testId flat-list, since component AppScreen it always show loading text, is there any way I could mock that API call and make this to work ?
Jest allows you to mock any module using jest.mock.
You have to write an alternative to axios.get like this
const vehiclesData = [
// ... put default data here
]
const delay = (ms, value) =>
new Promise(res => setTimeout(() => res(value), ms))
const mockAxiosGet = async (path) => {
let result = null
if (path.includes('vehicles/list') {
const query = new URLSearchParams(path.replace(/^[^?]+\?/, ''))
const page = + query.get('page')
const pageSize = 10
const offset = (page - 1)*pageSize
result = vehiclesData.slice(offset, offset + pageSize)
}
return delay(
// simulate 100-500ms latency
Math.floor(100 + Math.random()*400),
{ data: result }
)
}
Then modify the test file as
import 'react-native';
import React from 'react';
import {cleanup, render, fireEvent} from '#testing-library/react-native';
import axios from 'axios'
// enable jest mock on 'axios' module
jest.mock('axios')
import AppScreen from '../../../../src/screens/App/index';
import {Provider} from 'react-redux';
import {store} from '../../../../src/store/configureStore';
describe('App List Component', () => {
before(() => {
// mock axios implementation
axios.get.mockImplementation(mockAxiosGet)
})
beforeEach(() => jest.useFakeTimers());
afterEach(cleanup);
it('should render vehicle list page title', async () => {
const navigation = {
setParams: () => {},
navigate: jest.fn(),
};
const route = {
}
const component = (
<Provider store={store}>
<AppScreen route={route} navigation={navigation} />
</Provider>);
const {getByText, getByTestId} = render(component);
const pageTitle = await getByText('App Loading'); // this works fine
expect(pageTitle).toBeDefined();
});
it('should navigate to add vehicle', async () => {
const navigation = {
setParams: () => {},
navigate: jest.fn(),
};
const route = {
}
const component = (
<Provider store={store}>
<AppScreen route={route} navigation={navigation} />
</Provider>);
const {getByText, getByTestId} = render(component);
const flatList = await getByTestId('flat-list');// this throws error since flat list is still not shown, and loading is showing instead
});
For your use case, read more at Mocking Implementations

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

Categories

Resources