I am trying to build a stopwatch in react native expo which contains hours, minutes, seconds, and centiseconds which means 00:00:00:00, the issue that i am having is that the time that i am seeing on screen is slower than real stopwatch of google for example it is slower or faster but never the right time. for example when it is 2sec on google stopwatch it is 1sec on my time if slower or 3sec if faster. On the emulator the time is always slower so i test on the expo go or i build an apk and test on my real device. below are the codes that gave me the closest time at the beginning then when time increment it becomes faster than google stopwatch. I need your help please understand what is happening ? the time of setInterval() should be in milliseconds but it is not giving a right millis it is slower or faster.
Edit:
When i make the interval run after 100 millis, the result is right but when i make it 10millis it is not accurate
import React, { useState, useRef, useEffect, useCallback } from 'react';
import { View, Text, TouchableOpacity, StyleSheet, Image, Modal, Platform } from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { RadioButton } from 'react-native-paper';
import Ionicons from '@expo/vector-icons/Ionicons';
import { Video, ResizeMode } from 'expo-av';
import * as BackgroundFetch from 'expo-background-fetch';
import * as TaskManager from 'expo-task-manager';
export default function StopwatchScreen() {
const [menuVisible, setMenuVisible] = useState(false);
const [selectedOption, setSelectedOption] = useState(null);
const [checked, setChecked] = useState('first');
const radioOptions = ["Option 1", "Option 2", "Option 3"];
const [isRunning, setIsRunning] = useState(false);
const [time, setTime] = useState(0);
const [results, setResults] = useState([]);
const timer = useRef(null);
const handleLapButtonPress = useCallback(() => {
if(isRunning) {
setResults((previousResults) => [time, ...previousResults]);
} else {
setResults([]);
setTime(0);
}
}, [isRunning, time]);
const handleStartButtonPress = () => {
setIsRunning(!isRunning);
console.log("time: ", time);
console.log("isRunning: ", isRunning);
};
useEffect(() => {
let timer;
if(isRunning) {
timer = setInterval(timeStart, 20);
}
return () => clearInterval(timer);
}, [time, isRunning]);
const timeStart = () => {
setTime(prevStartTime => prevStartTime + 1);
console.log(time);
};
const toggleMenu = () => {
setMenuVisible(!menuVisible);
}
const padToTwo = (number) => (number <= 9 ? `0${number}` : number);
const displayTime = (milliseconds) => {
let hours = 0;
let minutes = 0;
let seconds = 0;
let centiseconds = 0;
let formatedMinutesSeconds;
let formatedCentiseconds;
//example 1000
if (milliseconds < 0) {
milliseconds = 0;
}
if (centiseconds < 100) {
formatedCentiseconds = `${padToTwo(centiseconds)}`;
}
centiseconds = Math.floor(milliseconds / 10) ;
let remainCentiseconds = centiseconds % 100;
seconds = Math.floor(milliseconds / 1000);
// millis = 55000;
// centiseconds = 5510;
// seconds = 55.1
if (seconds < 60) {
formatedCentiseconds = `${padToTwo(remainCentiseconds)}`;
formatedMinutesSeconds = `00:${padToTwo(seconds)}`;
} else {
formatedCentiseconds = `${padToTwo(remainCentiseconds)}`;
let remainSeconds = seconds % 60;
minutes = (seconds - remainSeconds) / 60;
if (minutes < 60) {
formatedMinutesSeconds = `${padToTwo(minutes)}:${padToTwo(remainSeconds)}`;
} else {
let remainMinutes = minutes % 60;
hours = (minutes - remainMinutes) / 60;
formatedMinutesSeconds = `${padToTwo(remainMinutes)}:${padToTwo(remainSeconds)}`;
}
}
return {
hours: padToTwo(hours),
minutesSeconds: formatedMinutesSeconds,
centiseconds: formatedCentiseconds,
};
};
return (
<View style={[styles.container, Platform.OS === 'android' && styles.androidPadding]}>
<View style={styles.header}>
<Text style={[styles.title, styles.textFont]}>Futuristic Stopwatch</Text>
<TouchableOpacity onPress={toggleMenu}>
<Image source={require('../assets/images/three_dots.png')} style={styles.dotsIcon} />
</TouchableOpacity>
</View>
<Modal
transparent={true}
animationType="slide"
visible={menuVisible}
onRequestClose={() => setMenuVisible(false)}
>
<View style={styles.modalContainer}>
<View style={styles.modalContent}>
<Ionicons name="close-circle" size={32} color="#11167F" onPress={() => setMenuVisible(false)} style={styles.closeButton} />
<View style={styles.sectionTitle}>
<Text style={[styles.sectionTitleText, styles.textFont]}>Theme</Text>
</View>
<View style={styles.radioBox}>
<View style={styles.radioOptions}>
<RadioButton
value="first"
status={ checked === 'first' ? 'checked' : 'unchecked' }
onPress={() => setChecked('first')}
color="blue"
/>
<Text style={[styles.textFont]}>Blue</Text>
</View>
<View style={styles.radioOptions}>
<RadioButton
value="second"
status={ checked === 'second' ? 'checked' : 'unchecked' }
onPress={() => setChecked('second')}
color="#DE382A"
/>
<Text style={[styles.textFont]}>Red</Text>
</View>
</View>
</View>
</View>
</Modal>
<View style={styles.contentContainer}>
<View style={styles.rowHud}>
{/* <Text style={[styles.timerOverlay, styles.textFont]}>00:00</Text> */}
<Text style={[styles.hoursTimerOverlay, styles.textFont]}>{`${displayTime(time).hours}`}</Text>
<Text style={[styles.timerOverlay, styles.textFont]}>{time}</Text>
<Text style={[styles.centisecondsTimerOverlay, styles.textFont]}>{`${displayTime(time).centiseconds}`}</Text>
</View>
<View style={styles.controls}>
<TouchableOpacity onPress={handleStartButtonPress} style={[styles.controlButtonBorder, { backgroundColor: isRunning ? "#340e0d" : "#0a2a12" }]}>
<View style={styles.controlButton}>
<Text style={{ color: isRunning ? "#ea4c49" : "#37d05c" }}>{isRunning ? 'Stop' : 'Start'}</Text>
</View>
</TouchableOpacity>
<TouchableOpacity onPress={handleLapButtonPress} style={[styles.controlButtonBorder, { backgroundColor: isRunning ? "#333333" : "#1c1c1e" }]}>
<View style={styles.controlButton}>
<Text style={{ color: isRunning ? "#fff" : "#9d9ca2" }}>{isRunning ? 'Lap' : 'Reset'}</Text>
</View>
</TouchableOpacity>
</View>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
padding: 0,
},
androidPadding: {
paddingTop: Platform.OS === 'android' ? 25 : 0,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 0,
paddingTop: 10,
paddingBottom: 10,
paddingLeft: 10,
paddingRight: 10,
backgroundColor: '#0a1055',
},
title: {
fontSize: 18,
//fontWeight: 'bold',
color: 'white',
},
dotsIcon: {
width: 20,
height: 20,
resizeMode: 'contain',
},
modalContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'rgba(0, 0, 0, 0.5)',
},
textFont: {
fontFamily: 'Orbitron Black',
},
modalContent: {
backgroundColor: '#fff',
padding: 20,
borderRadius: 10,
elevation: 5,
},
closeButton: {
position: 'absolute',
top: 10,
right: 10,
},
sectionTitle: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 10,
},
sectionTitleText: {
fontFamily: 'Orbitron Black',
},
radioBox: {
marginTop: 20,
alignItems: 'flex-start',
width: 300,
},
radioOptions: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 10,
},
contentContainer: {
flex: 1,
backgroundColor: '#0E1B84',
},
rowArrow: {
flex: 0.1,
flexDirection: 'row',
marginBottom: 0,
},
rowHud: {
flex: 0.5,
flexDirection: 'row',
marginBottom: 0
},
timerOverlay: {
position: 'absolute',
top: '50%', // Adjust as needed
left: '50%', // Adjust as needed
transform: [{ translateX: -90 }, { translateY: -30 }],
fontSize: 50,
color: 'white',
},
hoursTimerOverlay: {
position: 'absolute',
top: '35%', // Adjust as needed
left: '50%', // Adjust as needed
transform: [{ translateX: -30 }, { translateY: -30 }],
fontSize: 35,
color: 'white',
},
centisecondsTimerOverlay: {
position: 'absolute',
top: '71%', // Adjust as needed
left: '50%', // Adjust as needed
transform: [{ translateX: -30 }, { translateY: -30 }],
fontSize: 35,
color: 'white',
},
controls: {
flexDirection: 'row',
justifyContent: 'space-between',
paddingLeft: 20,
paddingRight: 20,
},
controlButtonBorder: {
justifyContent: "center",
alignItems: "center",
width: 70,
height: 70,
borderRadius: 70,
},
controlButton: {
justifyContent: "center",
alignItems: "center",
width: 65,
height: 65,
borderRadius: 65,
borderColor: "#000",
borderWidth: 1
}
});
Demo link – https://snack.expo.dev/cECs-YK53
First you should remove, the time
from the useEffect
dependency array. Because, inside setInterval
you are calling setTime
function which would cause a re-render. Now the value of time
has changed, so the useEffect will run again and creates a new setInterval
and this goes like an calling infinite setInterval unless you stop.
You should call setInterval
only when the start button is pressed and clear it when the stop button is pressed. To clear the interval on button press you can store the return value of setInterval in a ref.
useEffect(() => {
if(isRunning) {
timerId.current = setInterval(timeStart, 10);
}
return () => clearInterval(timerId.current);
}, [isRunning]);
Next, in the setInterval
instead of 20
as delay, use 10
as the delay since you need to display centiseconds aslo. (1 centiseconds = 10 milliseconds)
const handleStartButtonPress = () => {
setIsRunning(isRunning => {
if(isRunning && timerId.current){
clearTimeout(timerId.current)
}
return !isRunning
});
console.log("time: ", time);
console.log("isRunning: ", isRunning);
};
And then displayTime
function can be simplified more if you need:
// @param - centiSeconds refers to `time` stored in state
const displayTime = (centiSeconds) => {
let newCentiSeconds = centiSeconds % 100;
let seconds = Math.floor(centiSeconds / 100) % 60;
let minutes = Math.floor(centiSeconds / (100 * 60)) % 60;
let hours = Math.floor(centiSeconds / (100 * 60 * 60)) % 12;
return {
hours: hours.toString().padStart(2,0),
minutes: minutes.toString().padStart(2,0),
seconds: seconds.toString().padStart(2,0),
centiSeconds: newCentiSeconds.toString().padStart(2,0),
};
};
const {hours,minutes,seconds,centiSeconds} = displayTime(time)
return(
...
<Text style={[styles.hoursTimerOverlay, styles.textFont]}>{hours}</Text>
<Text style={[styles.timerOverlay, styles.textFont]}>:{minutes}</Text>
<Text style={[styles.timerOverlay, styles.textFont]}>:{seconds}</Text>
<Text style={[styles.centisecondsTimerOverlay, styles.textFont]}>:{centiSeconds}</Text>
...
)