I tried to program a little stopwatch to test something out but after clicking "Start" and its running the "Stop", "Lap" and "Reset" Buttons register the input up to a second or more after I click them. What am I missing here?
My guess is it has something to do with the useEffect hook, but Im not sure since I haven't used React or React Native that extensively.
export default function TabOneScreen({ navigation }: RootTabScreenProps<'TabOne'>) {
const [time, setTime] = useState<number>(0);
const [timerOn, setTimerOn] = useState(false);
const [lapCounter, setLapCounter] = useState<number>(0);
const [laps, setLaps] = useState<{count: number, lapTime: number}[]>([])
useEffect(() => {
var interval: any = null;
if (timerOn) {
interval = setInterval(() => {
setTime((prevTime) => prevTime + 10);
}, 10);
} else if (!timerOn) {
clearInterval(interval);
}
return () => clearInterval(interval);
}, [timerOn]);
return (
<View style={styles.container}>
<Text>time:</Text>
<View style={styles.timer}>
<Text>{("0" + Math.floor(time / 60000) % 60).slice(-2)}:</Text>
<Text>{("0" + Math.floor(time / 1000) % 60).slice(-2)}:</Text>
<Text>{("0" + (time / 10) % 100).slice(-2)}</Text>
</View>
<View style={styles.buttons}>
<Button
title="Start"
onPress={() => setTimerOn(true)}
/>
<Button
title="Stop"
onPress={() => setTimerOn(false)}
/>
<Button
title="Lap"
onPress={() => {
setLapCounter(counter => counter += 1)
setLaps(prevLaps => [
...prevLaps,
{count: lapCounter, lapTime: time}
]
)
}}
/>
<Button
title="Reset"
onPress={() => {
setTimerOn(false)
setTime(0)
setLapCounter(0)
setLaps([])
}
}
/>
</View>
<FlatList
data={laps}
renderItem={({ item }) =>
<View style={styles.lapList}>
<Text style={styles.item}>Lap: {item.count}</Text>
<Text style={styles.item}>{item.lapTime}</Text>
</View>
}
/>
</View>
);
}
On the "else if" you clear an empty interval (because you did not save the previous one anywhere). Create a new hook, such as useTimer.
Or use a premade like: https://www.npmjs.com/package/use-timer
Related
I am trying to count the number of times a button is pressed within a second.
It's working, for the most part, it tracks it and outputs it.
But the problem is that it outputs the button press count from the last second instead of the current second.
I think it would work if the order was reversed somehow, how do I fix up this function? Thanks.
const [clicksPerSecond, setClicksPerSecond] = useState(0);
const [tempCounter, setTempCounter] = useState(0);
const [tempCounter2, setTempCounter2] = useState(0);
const { setCounter, counter } = useContext(CountContext);
useEffect(() => {
console.log(tempCounter);
if (tempCounter != 0) {
if (tempCounter == 1) {
setTimeout(() => {
setClicksPerSecond(tempCounter2);
setClicksPerMinute(tempCounter2 * 60);
setTempCounter(1);
console.log('Clicks per second final: ' + tempCounter2);
}, 1000)
} else {
setTempCounter2(tempCounter);
console.log('Clicks per second: ' + tempCounter);
}
}
setTempCounter(tempCounter + 1);
}, [counter])
return (
<View style={[styles.container, { backgroundColor: colors.background }]}>
<View elevation={7.5} style={{ backgroundColor: colors.background, borderRadius: 500 }}>
<TouchableOpacity
onPress={() => setCounter(counter + 1)}
style={[styles.counterButton, { backgroundColor: colors.primary, borderColor: colors.container, borderWidth: 0 }]}>
<Text></Text>
<Text style={{ fontSize: 60 }}>{counter}</Text>
<Text>{clicksPerSecond} CPS</Text>
</TouchableOpacity>
</View>
</View>
);
}
You can instead just increment the count and decrement it after a second.
export default function App() {
const [clicks, setClicks] = React.useState(0);
const onClick = () => {
setClicks((c) => c + 1);
setTimeout(() => setClicks((c) => c - 1), 1000);
};
return (
<div>
<button onClick={onClick}>Click me</button>
<p>Clicked {clicks} times</p>
</div>
);
}
You will also need to track if the component is unmounted before decrement, which I think you can do using refs. Example
I have recently started working on react native, and I am currently working on the below code for timer and countdown functionality. Before I am able to put specific time and it is working fine. Also I am able to increment and decrement static values, but on timer getting error.
In the below code while incrementing and decrementing the time, I am getting 'NaN' as output.
//Timing.js
export const Timing = ({ onChangeTime }) => {
return (
<>
<View style={styles.timingButton}>
<RoundedButton size={75} title="+" onPress={() => onChangeTime(onChangeTime + 1)} />
</View>
<View style={styles.timingButton}>
<RoundedButton size={75} title="15" onPress={() => onChangeTime(15)} />
</View>
<View style={styles.timingButton}>
<RoundedButton size={75} title="-" onPress={() => onChangeTime(onChangeTime -1)} />
</View>
</>
);
};
//Timer.js
export const Timer = ({ focusSubject, clearSubject, onTimerEnd }) => {
useKeepAwake();
const [isStarted, setIsStarted] = useState(false);
const [progress, setProgress] = useState(1);
const [minutes, setMinutes] = useState(0.1);
const onEnd = (reset) => {
Vibration.vibrate(PATTERN);
setIsStarted(false);
setProgress(1);
reset();
onTimerEnd(focusSubject);
};
return (
<View style={styles.container}>
<View style={styles.countdown}>
<Countdown
minutes={minutes}
isPaused={!isStarted}
onProgress={setProgress}
onEnd={onEnd}
/>
<View style={{ paddingTop: spacing.xxl }}>
<Text style={styles.title}>Focusing on:</Text>
<Text style={styles.task}>{focusSubject}</Text>
</View>
</View>
<View style={{ paddingTop: spacing.sm }}>
<ProgressBar
progress={progress}
color={colors.progressBar}
style={{ height: spacing.sm }}
/>
</View>
<View style={styles.timingWrapper}>
<Timing onChangeTime = {setMinutes} />
</View>
<View style={styles.buttonWrapper}>
{!isStarted ? (
<RoundedButton title="start" onPress={() => setIsStarted(true)} />
) : (
<RoundedButton title="pause" onPress={() => setIsStarted(false)} />
)}
</View>
<View style={styles.clearSubjectWrapper}>
<RoundedButton size={50} title="-" onPress={clearSubject} />
</View>
</View>
);
};
//Countdown.js
const minutesToMillis = (min) => min * 1000 * 60;
const formatTime = (time) => (time < 10 ? `0${time}` : time);
export const Countdown = ({ minutes = 0.1, isPaused, onProgress, onEnd }) => {
const interval = React.useRef(null);
const [millis, setMillis] = useState(null);
const reset = () => setMillis(minutesToMillis(minutes))
const countDown = () => {
setMillis((time) => {
if (time === 0) {
clearInterval(interval.current);
onEnd(reset);
return time;
}
const timeLeft = time - 1000;
return timeLeft;
});
};
useEffect(() => {
setMillis(minutesToMillis(minutes));
}, [minutes]);
useEffect(() => {
onProgress(millis / minutesToMillis(minutes));
}, [millis]);
// useEffect(() => {
// onIncrement(millis / minutesToMillis(minutes)+1000);
// },[millis]);
useEffect(() => {
if (isPaused) {
if (interval.current) clearInterval(interval.current);
return;
}
interval.current = setInterval(countDown, 1000);
return () => clearInterval(interval.current);
}, [isPaused]);
const minute = Math.floor(millis / 1000 / 60) % (60);
const seconds = Math.floor(millis / 1000) % (60);
return (
<Text style={styles.text}>
{formatTime(minute)}:{formatTime(seconds)}
</Text>
);
};
Change your Timing likewise
export const Timing = ({ onChangeTime }) => {
return (
<>
<View style={styles.timingButton}>
<RoundedButton size={75} title="+" onPress={() => onChangeTime(prev => prev + 1)} />
</View>
<View style={styles.timingButton}>
<RoundedButton size={75} title="15" onPress={() => onChangeTime(15)} />
</View>
<View style={styles.timingButton}>
<RoundedButton size={75} title="-" onPress={() => onChangeTime( prev => prev - 1)} />
</View>
</>
);
};
I am having an Invalid Hook Error in RN. I am using a button click event handler to execute a setInterval function for a countdown timer.
Error: 'Hooks can only be called inside the body of a function component. (...)'
My code:
import { Button, StatusBar, StyleSheet, Text, TouchableOpacity, View } from 'react-native'
import React, { useEffect, useState } from 'react'
import { Ionicons } from '#expo/vector-icons'
import { AntDesign } from '#expo/vector-icons'
export default function MenuBar() {
const [time, SetTime] = useState(10);
const startTime = () => {
useEffect(() => {
const interval = setInterval(() => {
if(time > 0) {
SetTime(time => time - 1);
} else {
SetTime(time => time + 10);
}
}, 1000);
return () => clearInterval(interval);
}, []);
}
return (
<View style={styles.container}>
<Button color="orange" onPress={startTime} title="Start Time!!"></Button>
<View style={styles.menu}>
<TouchableOpacity>
<AntDesign style={[styles.button, styles.exitBtn] } name="logout" size={24} color="white" />
</TouchableOpacity>
<TouchableOpacity>
<AntDesign style={styles.button} name="questioncircleo" size={24} color="white" />
</TouchableOpacity>
<Text style={styles.timer}>{time}</Text>
<TouchableOpacity>
<AntDesign style={styles.button} name="picture" size={24} color="white" />
</TouchableOpacity>
<TouchableOpacity>
<AntDesign style={styles.button} name="sound" size={24} color="white" />
</TouchableOpacity>
</View>
</View>
)
}
You cannot call a hook inside of another function unless that function is a React Component.
As you want to start the timer when pressing a button you don't need to listen to side effects and therefore don't need to call useEffect, and just start the timer when pressing the button.
You do need to clear the timer when unmounting the component. For this you will need a useEffect, as React internally will trigger the useEffect cleanup function.
I would suggest something like this:
export default function MenuBar() {
const interval = useRef(null)
const [time, setTime] = useState(10);
const startTime = () => {
if (interval.current) {
// Making sure not to start multiple timers if one
// has already started
clearInterval(interval.current);
}
interval.current = setInterval(() => {
if (time > 0) {
setTime(time => time - 1);
} else {
setTime(time => time + 10);
}
}, 1000);
}
// only use useEffect when unmounting the component
// and calling the cleanup function
useEffect(() => {
return () => {
if (interval.current) {
return clearInterval(interval.current);
}
};
}, []);
return (
// rest of component
)
I'm trying to make a slideshow where the user can play them in a default or random order and cycle through them by pressing a button. The problem comes when the user has chosen random mode; pressing the button still increments the currentIndex variable, but for some reason images[currentIndex] does not change.
const SlideshowScreen = (props) => {
const settings = props.route.params.settings;
const [images, setImages] = useState(settings.random ? props.route.params.images.sort((a, b) => 0.5 - Math.random()) : props.route.params.images);
const [currentIndex, setCurrentIndex] = useState(0);
useEffect(() => {
console.log('currentIndex', currentIndex, images[currentIndex]);
}, [currentIndex])
const newImage = () => {
if (currentIndex + 1 >= images.length) {
console.log('done');
}
else {
setCurrentIndex(currentIndex + 1);
}
}
return (
<View style={styles.mainContainer}>
<View style={styles.imageContainer}>
<Image
style={styles.image}
source={images[currentIndex].path} key={images[currentIndex].id} />
</View>
<TouchableOpacity onPress={() => newImage()}>
<Text>Next image</Text>
</TouchableOpacity>
</View>
)
}
On some clicks of the button this happens, and on others it doesn't. How is this happening? Any help would be great, thanks.
I am working on an app where I need to filter by dates to show on a particular screen. The idea is anything within 30 days of today's date should be populated in the filter. I am trying to get that screen to render as soon as the app is opened but it only renders after playing around on other screens.
Here is the logic below. Any help is appreciated.
const ExpiringCertificationsScreen = () => {
const { state, fetchCertifications } = useContext(CertificationContext);
const [expiration, setExpiration] = useState([]);
const diffDays = date => {
let today = new Date(Date.now()).toLocaleDateString();
let expire = new Date(date.toLocaleDateString());
let difference = new Date(expire).getTime() - new Date(today).getTime();
return difference / (1000 * 3600 * 24);
};
const filterExpirationDate = state.filter(item => {
return diffDays(new Date(item.expirationDate)) <= 30;
});
useEffect(() => {
if (filterExpirationDate.length > 0) {
console.log(filterExpirationDate);
setExpiration(filterExpirationDate);
} else {
console.log('Down');
}
}, []);
if (expiration.length === 0) {
return (
<Body>
<Text style={styles.title}>EXPIRING SOON</Text>
<Text style={{ margin: 15 }}>
There aren't any certifications expiring soon
</Text>
</Body>
);
}
return (
<Body>
<Text style={styles.title}>EXPIRING SOON</Text>
<ScrollView style={{ marginTop: 15 }}>
<FlatList
data={expiration}
keyExtractor={item => item._id}
renderItem={({ item }) => {
return (
<Certifications
title={item.title}
month={item.expirationDate}
description={item.description}
id={item._id}
/>
);
}}
/>
</ScrollView>
</Body>
);
};
I have updated your code as per your requirement.. Please check and acknowledge
const ExpiringCertificationsScreen = () => {
const { state, fetchCertifications } = useContext(CertificationContext);
const [expiration, setExpiration] = useState([]);
const diffDays = date => {
let today = new Date(Date.now()).toLocaleDateString();
let expire = new Date(date.toLocaleDateString());
let difference = new Date(expire).getTime() - new Date(today).getTime();
return difference / (1000 * 3600 * 24);
};
useEffect(() => {
const filterExpirationDate = state.filter(item => {
return diffDays(new Date(item.expirationDate)) <= 30;
});
console.log(filterExpirationDate);
setExpiration(filterExpirationDate);
}, [state]);
return expiration.length === 0 ? (
<Body>
<Text style={styles.title}>EXPIRING SOON</Text>
<Text style={{ margin: 15 }}>
There aren't any certifications expiring soon
</Text>
</Body>
) :(
<Body>
<Text style={styles.title}>EXPIRING SOON</Text>
<ScrollView style={{ marginTop: 15 }}>
<FlatList
data={expiration}
keyExtractor={item => item._id}
renderItem={({ item }) => {
return (
<Certifications
title={item.title}
month={item.expirationDate}
description={item.description}
id={item._id}
/>
);
}}
/>
</ScrollView>
</Body>
);
};