I am attempting to enable a Thumbs Up option where when pressed it retrieves the current number of thumbs up in my database for an item, increments it, then updates it. As you can see I've put 2 "here" console.logs, both log to the screen. I get no errors or catches, and the database updates (immediately when pressed). So it technically works, but then there is a lag/hang, for probably 7 seconds where the thumb icon shows as pressed the whole time like it's stuck, and then the modal goes away back to the main screen. As if it "crashed". If I take the update out, there is no hang and the modal remains open until I choose to close it. So it has to be that code. Do you see any glaring errors in my logic?
Update: I've traced the issue possibly to the screen that renders this component. I populate a flatlist using the firebase onValue, of which each object has this "thumbs up" feature. When I take out the onValue section and replace it with a single dummy data object instead, the thumbs up works perfectly. So there must be a problem with the listener maybe not unsubscribing right the way I have it. Previous screen code added.
import React, {useState} from "react";
import { Image, StyleSheet, TouchableOpacity, View } from "react-native";
import AppText from "../components/AppText";
import * as Sentry from 'sentry-expo';
import { ref, child, get, query, onValue, update } from "firebase/database";
function HotDidItWork({indexStore, db, auth}) {
var user = auth.currentUser;
if (!user){
return (
<></>
)
}
const [upPressed, setUpPressed] = useState(false);
const [displayText, setDisplayText] = useState('');
async function didPressUp(){
setUpPressed(true);
const dbRef = ref(db, 'deals/' + indexStore);
if (displayText == ''){
get(query(dbRef))
.then((usersSnapshot)=> {
if (usersSnapshot.exists()) {
let thumbsUpCount = usersSnapshot.val().thumbsUp;
thumbsUpCount = parseInt(thumbsUpCount);
thumbsUpCount += 1;
console.log('here 1');
update(dbRef, {thumbsUp: thumbsUpCount.toString()})
.then(()=> {
console.log('here 2');
setDisplayText('Thanks!');
})
.catch((e)=> {
console.log('error 1: ' + e.message);
})
} else {
console.log("No data available");
}
})
.catch((e)=> {
console.log("error 2: " + e.message);
})
}
}
return (
<View>
<View style={styles.messageContainer}>
<AppText style={upPressed == false && downPressed == false ? styles.didItWork : styles.pressed}>Did this deal work for you?</AppText>
<AppText style={downPressed == true || upPressed == true ? styles.didItWork : styles.pressed}>{displayText}</AppText>
</View>
<View style={styles.thumbs}>
<TouchableOpacity onPress={() => didPressUp()}>
<Image
style={styles.thumbsUp}
source={require("../assets/thumbsUp.png")}
/>
</TouchableOpacity>
</View>
</View>
);
}
export default HotDidItWork;
Previous Screen that renders flatlist of obj with thumbs up feature (I now believe this is where the error is):
import React, { useState, useEffect } from "react";
import {
FlatList,
Image,
ImageBackground,
Platform,
SafeAreaView,
StyleSheet,
TextInput,
TouchableOpacity,
View,
} from "react-native";
import HotCardFav from "../components/HotCardFav";
import { ref, child, get, query, onValue, update } from "firebase/database";
import ListItemSeparator from "../components/ListItemSeparator";
import CardItemDeleteAction from "../components/CardItemDeleteAction";
import { MaterialCommunityIcons } from "#expo/vector-icons";
import { ActivityIndicator } from "react-native";
import {db, auth} from '../../src/config.js';
import AppText from "../components/AppText";
import colors from "../config/colors";
import * as Sentry from 'sentry-expo';
import Header from '../components/Header';
import { ThemeProvider, useFocusEffect } from '#react-navigation/native';
let initialMessagesFav = [];
let listViewRef;
function FavoritesHotScreen() {
var user = auth.currentUser;
if (user == null) {
return (
<></>
)
}
const [loading, setLoading] = useState(false);
const [messagesFav, setMessagesFav] = useState(initialMessagesFav);
const [messagesFavHold, setMessagesFavHold] = useState(initialMessagesFav);
const [refreshing, setRefreshing] = useState(false);
useFocusEffect(
React.useCallback( () => {
async function fetchData() {
// You can await here
const response = await loadListings();
// ...
return () => response();
}
fetchData();
}, [])
);
async function lookupUser(){
const dbRef = ref(db, 'users/' + user.uid);
const usersSnapshot = await get(query(dbRef));
return usersSnapshot;
}
const loadListings = async () => {
let favs = [];
let favsArray = [];
updateInput('');
setLoading(true);
console.log('exists 2');
lookupUser()
.then((snapshot) => {
if (snapshot.exists()) {
favs = snapshot.child("favorites").val();
if (favs != null){
favsArray = favs.split(',');
}
const dbRef = ref(db, 'deals');
return onValue(dbRef , (snapshot) => {
let testData = [];
let searchData = [];
snapshot.forEach((childSnapshot)=>{
let found = favsArray.find(function (element) {
return element == childSnapshot.val().indexStore;
});
if (found != undefined){
testData.push({
id: childSnapshot.key,
title: childSnapshot.val().title,
postedDate: childSnapshot.val().postedDate,
desc: childSnapshot.val().desc,
indexStore: childSnapshot.val().indexStore,
})
checkMessages(testData);
setLoading(false);
}
})
})
.catch((error) => Sentry.Native.captureException('Error FavoritesScreen function loadListings 2 ' + error));
}
const renderItem = ({ item }) => (<HotCardFav
title={item.title}
desc={item.desc}
indexStore={item.id}
postedDate={item.postedDate}
/>);
function checkMessages(testData){
const filtered = testData.filter(country => {
return (country.title != 'NA')
})
setMessagesFav(filtered);
setMessagesFavHold(testData);
setLoading(false);
}
let messagesShow = messagesFav.sort((a, b) => {
const messageA = new Date(parseInt(a.postedDate));
const messageB = new Date(parseInt(b.postedDate));
let comparison = 0;
if (messageA > messageB) {
comparison = 1;
} else if (messageA < messageB) {
comparison = -1;
}
return comparison * -1;
});
return (
<SafeAreaView style={styles.wholeThing}>
<Header image={require('../assets/longlogo4.png')} />
<View style={loading ? styles.activity : styles.none}>
<ActivityIndicator animating={loading} size="large" color="#0000ff" />
</View>
<FlatList
data={messagesShow}
keyExtractor={(messagesShow) => messagesShow.id.toString()}
renderItem={renderItem}
ItemSeparatorComponent={ListItemSeparator}
contentContainerStyle={styles.messagesList}
refreshing={refreshing}
ref={(ref) => {
listViewRef = ref;
}}
/>
</SafeAreaView>
);
}
export default FavoritesHotScreen;
Ok I figured it out here's what I found in case it helps someone in the future. The problem was in my main screen loadlistings() function (that I posted as an edit). It uses onValue to retrieve firebase data which attaches a listener which means ANYTIME data on my database changes, it rerenders the flatlist, entirely. Which automatically closes my modal since it's defaulted to be closed when the screen starts. So by me pressing the "thumbs up", it was changing data, the listener responded, rerendered the flatlist with a closed modal. Maybe this should have been obvious but it wasn't for me. I fixed it by adding a simple "onlyOnce" flag to the end of the onValue per firebase documentation:
return onValue(dbRef , (snapshot) => {
let testData = [];
let searchData = [];
snapshot.forEach((childSnapshot)=>{
let found = favsArray.find(function (element) {
return element == childSnapshot.val().indexStore;
});
if (found != undefined){
testData.push({
id: childSnapshot.key,
title: childSnapshot.val().title,
postedDate: childSnapshot.val().postedDate,
desc: childSnapshot.val().desc,
indexStore: childSnapshot.val().indexStore,
})
checkMessages(testData);
setLoading(false);
})
},{
onlyOnce: true
})
Related
I'm working on a mobile phone application with Stripe and Expo Bar Code Scanner. When you start the application, if you gave the permissions for using the camera, you will can scan bar codes. Bar Codes only contains the id of the scanned item. If it exists, two buttons (+/-) will appear in order to choose the amount for the item. If it doesn't exists, nothing happens. When the amount changes, I save in SecureStore the id of the item as the key and the amount as the value.
The problem is when I move on others screens (with React Navigation) and I came back to scan and I rescan the same item, the amount resets to 0. If you don't give the permissions for the camera, it displays a list of available items when you can choose the amount (+/-) buttons and similar problem.
Here the concerned two files :
ItemListComponent.tsx
import { Button, FlatList, View, Text } from 'react-native';
import * as SecureStore from 'expo-secure-store';
import { useState } from 'react';
export const ItemComponent = (props: any) => {
const [amount, setAmount] = useState<number>(0);
const getAmount = async () => {
const amount = await SecureStore.getItemAsync(props.item.id.toString());
if (amount) {
setAmount(parseInt(amount));
}
getAmount();
}
const save = async () => {
await SecureStore.setItemAsync(props.item.id.toString(), amount.toString());
}
return (
<View>
<Text>{props.item.name}</Text>
<Button
onPress={() => {
setAmount(amount + 1);
save();
}}
title='+'
/>
{amount > 0 &&
<Button
onPress={() => {
setAmount(amount - 1);
save();
}}
title='-'
/>
}
</View>
);
};
export const ItemListComponent = (props: any) => {
return (
<FlatList
data={props.items}
renderItem={({ item }) =>
<ItemComponent key={item.id} item={item} />
}
/>
);
};
BarCodeScannerComponent.tsx
import { BarCodeScanner } from 'expo-barcode-scanner';
import { useState } from 'react';
import { StyleSheet } from 'react-native';
import { ItemComponent } from './ItemListComponent';
import Items from '../models/ItemsModel';
export const BarCodeScannerComponent = () => {
const [item, setItem] = useState<Items>();
const getItem = async ({ data }: any) => {
const response = await fetch(`http://192.168.1.81:8000/items/${data}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (response.ok) {
const json = await response.json();
setItem(json);
}
}
return (
<View style={styles.container}>
<BarCodeScanner
onBarCodeScanned={getItem}
style={StyleSheet.absoluteFillObject}
/>
{(item !== null && item !== undefined) && <ItemComponent key={item.id} item={item} />}
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
flexDirection: 'column',
justifyContent: 'center',
},
});
Thanks for help !
It looks like you never call getAmount, and if you did call it you'd get infinite recursion.
const getAmount = async () => {
const amount = await SecureStore.getItemAsync(props.item.id.toString());
if (amount) {
setAmount(parseInt(amount));
}
getAmount();
}
should be
const getAmount = async () => {
const amount = await SecureStore.getItemAsync(props.item.id.toString());
if (amount) {
setAmount(parseInt(amount));
}
}
getAmount();
or, probably even better:
const getAmount = async () => {
const storeAmount = await SecureStore.getItemAsync(props.item.id.toString());
if (amount !== parseInt(storeAmount)) {
setAmount(parseInt(storeAmount));
}
}
useEffect(() => {
getAmount();
}, [props.item.id]);
otherwise, every time it renders you'll call setAmount which will trigger a rerender
If I add tasksMap to the useEffect dependency array below an infinite loop will happen. Can someone point me in the right direction on how to fix this? In order for the user to get an updated view of the tasks that have been added or modified, I need the app to call getProjectTasks and assign the returned map to the tasksMap. I do know that anytime you update state the component rerenders. I just havne't figured out how to do this without creating an infinite loop. Any help is greatly appreciated. Thank you.
import { useContext, useState, useEffect } from "react";
import { useParams } from "react-router-dom";
import { UserContext } from "../../contexts/user.context";
import { ProjectsContext } from "../../contexts/projects.context";
import { createProjectTask, getProjectTasks } from "../../utils/firebase/firebase.utils";
import OutlinedCard from "../../components/cards/TaskCard.component";
import { TextField, Button, } from "#mui/material";
import "./project.styles.css";
import "./project.styles.css";
const Project = () => {
const params = useParams();
const projectId = params.id;
const { currentUser } = useContext(UserContext);
const { projectsMap } = useContext(ProjectsContext);
const [taskName, setTaskName] = useState("");
const [tasksMap, setTasksMap] = useState({});
const project = Object.keys(projectsMap)
.filter((id) => id.includes(projectId))
.reduce((obj, id) => {
return Object.assign(obj, {
[id]: projectsMap[id],
});
}, {});
useEffect(() => {
console.log("running")
const getTasksMap = async () => {
const taskMap = await getProjectTasks(currentUser, projectId);
taskMap ? setTasksMap(taskMap) : setTasksMap({});
};
getTasksMap();
}, [projectId])
const handleChange = (event) => {
const { value } = event.target;
setTaskName(value);
};
const handleSubmit = async (event) => {
event.preventDefault();
try {
await createProjectTask(currentUser, projectId, taskName);
setTaskName("");
} catch (error) {
console.log(error);
}
};
return (
<div className="project-container">
{project[projectId] ? <h2>{project[projectId].name}</h2> : ""}
<form onSubmit={handleSubmit} className="task-form">
<TextField label="Project Task" onChange={handleChange} value={taskName}></TextField>
<Button type="submit" variant="contained">
Add Task
</Button>
</form>
<div className="tasks-container">
{Object.keys(tasksMap).map((id) => {
const task = tasksMap[id];
return (
<OutlinedCard key={id} projectId={projectId} taskId={id} name={task.name}></OutlinedCard>
);
})}
</div>
</div>
);
};
export default Project;
This is where the taskMap object comes from. For clarification, I'm using Firebase.
export const getProjectTasks = async(userAuth, projectId) => {
if(!userAuth || !projectId) return;
const tasksCollectionRef = collection(db, "users", userAuth.uid, "projects", projectId, "tasks")
const q = query(tasksCollectionRef);
try {
const querySnapshot = await getDocs(q);
const taskMap = querySnapshot.docs.reduce((acc, docSnapshot) => {
const id = docSnapshot.id;
const { name } = docSnapshot.data();
acc[id] = {id, name};
return acc;
}, {});
return taskMap;
} catch (error) {
console.log("Error getting task docs.");
}
};
The useEffect appears to be setting tasksMap when executed. Because this state is an object its reference will change everytime, which will produce an infinite loop
I am new to react native. I am trying to make a podcast player. While I was able to make it work in my Podcast component, when I change to other component, the audio player current status is not recognized.
I am reusing AudioPlayer component (the one in green) that is working fine in the Podcast component. But how can I have access to the currently playing song from other pages like Home?
This is my AudioPlayer.js that I am trying to reuse in other components. I want to have controls like pause/play, next work from other other components. I am storing the current audio url in redux store but the only thing that I am to do so far is restart the current audio when I click pause from other component.
import React, {useState} from "react";
import { View, Text, Image, TouchableOpacity, StyleSheet } from "react-native";
import { AntDesign } from "#expo/vector-icons";
import { connect } from "react-redux";
import { Audio } from "expo-av";
import { audioPlayerAction } from "../actions";
import { radio } from "../assets/radio/radio";
import { styles } from "../assets/styles";
const audio = new Audio.Sound();
const AudioPlayer = (props) => {
// get currently playing song url from redux store
const { audioSrc, index } = props.storeState;
const [currentAudioIndex, setCurrentAudioIndex] = useState(index);
const [currentAudio, setCurrentAudio] = useState(null);
const [loaded, setLoaded] = useState(false);
const [paused, setPaused] = useState(false);
const [nowPlaying, setNowPlaying] = useState("");
const [playerStatus, setPlayerStatus] = useState(null);
Audio.setAudioModeAsync({
staysActiveInBackground: true,
});
// continue playing next song when done
const onPlaybackStatusUpdate = (playbackStatus) => {
if (playbackStatus.didJustFinish) {
next();
}
};
const play = async (url) => {
if (currentAudio !== url) {
await audio.unloadAsync();
try {
const status = await audio.loadAsync(
{
uri: url,
},
{ shouldPlay: true }
);
audio.setOnPlaybackStatusUpdate(onPlaybackStatusUpdate);
setCurrentAudio(url);
setPlayerStatus(status);
setLoaded(true);
setPaused(false);
setNowPlaying(radio[currentAudioIndex].station);
await audio.playAsync();
} catch (error) {
console.log(error);
}
} else if (currentAudio === url && loaded === true && paused === false) {
await audio.pauseAsync();
setPaused(true);
} else if (currentAudio === url && loaded === true && paused === true) {
await audio.playAsync();
setPaused(false);
}
};
const next = async () => {
let nextIndex = index + 1 >= radio.length ? 0 : index + 1;
setCurrentAudioIndex(nextIndex);
let nextAudio = radio[nextIndex].url;
props.audioPlayerAction(nextAudio, true, null, "podcast", nextIndex);
play(nextAudio);
};
const prev = () => {
let prevSongIndex = index - 1 < 0 ? radio.length - 1 : index - 1;
let prevAudio = radio[prevSongIndex].url;
props.audioPlayerAction(prevAudio, true, null, "podcast", prevSongIndex);
play(prevAudio);
};
return (
<View style={style.container}>
<View style={style.musicTitle}>
<Text style={style.musicTitleText}>Now Playing: {nowPlaying}</Text>
</View>
<View style={style.controls}>
<TouchableOpacity onPress={() => prev()}>
<AntDesign style={style.icon} name="stepbackward" />
</TouchableOpacity>
<TouchableOpacity onPress={() => play(audioSrc)}>
<AntDesign style={style.icon} name={paused ? "play" : "pause"} />
</TouchableOpacity>
<TouchableOpacity onPress={() => next()}>
<AntDesign style={style.icon} name="stepforward" />
</TouchableOpacity>
</View>
</View>
);
};
const MapStateToProps = (state) => {
return { storeState: state.audioPlayerReducer };
};
export default connect(MapStateToProps, { audioPlayerAction })(AudioPlayer);
So, I'm facing a problem when I navigate to my scanner screen and go back the previous screen, then navigate again to my scanner screen, barcode scanner does not working. even console logs does not working. I have to clear cashe and all data from expo app in order to work scanner screen again. I really don't know what causing the porblem but highly suspicious about Navigation. Can anyone help me pls?
Im adding my Scanner Screen right below.
import React, { useState, useEffect } from "react";
import {
Text,
View,
FlatList,
Button,
Modal,
Pressable,
Alert,
StyleSheet,
} from "react-native";
import { BarCodeScanner } from "expo-barcode-scanner";
import axios from "axios";
import { localIP, ngrokServer } from "../constants";
import allStyles from "../components/molecules/Styles";
const styles = allStyles;
export default function ScannerScreen({ navigation }) {
const [hasPermission, setHasPermission] = useState(null);
const [scanned, setScanned] = useState(false);
useEffect(() => {
setReset(false);
});
useEffect(() => {
(async () => {
const { status } = await BarCodeScanner.requestPermissionsAsync();
setHasPermission(status === "granted");
})();
}, []);
const handleBarCodeScanned = async ({ type, data }) => {
setScanned(true);
console.log("Data: ", data);
};
if (hasPermission === null) {
return <Text>Requesting for camera permission</Text>;
}
if (hasPermission === false) {
return <Text>No access to camera</Text>;
}
return (
<View style={styles.scannerScreenContainer}>
<BarCodeScanner
onBarCodeScanned={scanned ? undefined : handleBarCodeScanned}
style={StyleSheet.absoluteFillObject}
/>
{scanned && reset && (
<Button title={"Tap to Scan Again"} onPress={() => setScanned(false)} />
)}
</View>
);
}
I'm using axios.post and thought maybe that was the cause of problem but when I removed that code block and run again it doesn't scan the QR code.
I had the same issue and I fixed it by adding a listener for focus events (emitted when the screen comes into focus).
This is what it looks like:
useEffect(() => {
const unsubscribe = navigation.addListener('focus', () => {
// do something - for example: reset states, ask for camera permission
setScanned(false);
setHasPermission(false);
(async () => {
const { status } = await BarCodeScanner.requestPermissionsAsync();
setHasPermission(status === "granted");
})();
});
// Return the function to unsubscribe from the event so it gets removed on unmount
return unsubscribe;
}, [navigation]);
Try using this code and see if it works.
Source: https://reactnavigation.org/docs/navigation-events/#navigationaddlistener
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>
)
}