问题
I have this very simple app, where I am trying to implement countdown, Start
and Stop
functionality seems to be working fine but when I press Stop
, the value of seconds
is not updating in the view which is desired behaviour but when I observed console log, it it showing the value of sec
continuously changing which I guess shows that setInterval
is still running and not cleared.
Here is the accompanied code:
import React, { useState, useEffect } from "react";
import {
StyleSheet,
Text,
View,
StatusBar,
TouchableOpacity,
Dimensions,
} from "react-native";
const screen = Dimensions.get("screen");
export default function App() {
const [seconds, setSeconds] = useState(4);
const [start, setStartToggle] = useState(true);
let [fun, setFun] = useState(null);
const getRemaining = () => {
const minute = Math.floor(seconds / 60);
const second = seconds - minute * 60;
return formatTime(minute) + ":" + formatTime(second);
};
const formatTime = (time) => {
return ("0" + time).slice(-2);
};
const startTimer = () => {
setStartToggle(false);
console.log("StartTimer");
let sec = seconds;
setFun(
setInterval(() => {
console.log("akak:", sec);
if (sec <= 0) stopTimer();
setSeconds(sec--);
}, 1000)
);
};
const stopTimer = () => {
setStartToggle(true);
console.log("StopTimer");
clearInterval(fun);
setFun(null);
};
return (
<View style={styles.container}>
<StatusBar barStyle="light-content" />
<Text style={styles.timerText}>{getRemaining(seconds)}</Text>
{start ? (
<TouchableOpacity onPress={startTimer} style={styles.button}>
<Text style={styles.buttonText}>Start</Text>
</TouchableOpacity>
) : (
<TouchableOpacity
onPress={stopTimer}
style={[styles.button, { borderColor: "orange" }]}
>
<Text style={styles.buttonText}>Stop</Text>
</TouchableOpacity>
)}
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#07121B",
alignItems: "center",
justifyContent: "center",
},
button: {
borderWidth: 10,
borderColor: "#89aaff",
width: screen.width / 2,
height: screen.width / 2,
borderRadius: screen.width / 2,
justifyContent: "center",
alignItems: "center",
},
buttonText: {
fontSize: 40,
color: "#89aaff",
fontWeight: "bold",
},
timerText: {
fontSize: 90,
color: "#89aaff",
fontWeight: "bold",
marginBottom: 20,
},
});
App Behaviour:
Output being shown even after Stop is hit:
Thanks to Emiel Zuurbier
and lissettdm
for their answers, my code is working now.
Working code:
// import { StatusBar } from "expo-status-bar";
import React, { useState, useEffect, useRef } from "react";
import {
StyleSheet,
Text,
View,
StatusBar,
TouchableOpacity,
Dimensions,
} from "react-native";
const screen = Dimensions.get("screen");
export default function App() {
const [seconds, setSeconds] = useState(11);
const funRef = useRef(null);
const [start, setStartToggle] = useState(true);
const getRemaining = () => {
const minute = Math.floor(seconds / 60);
const second = seconds - minute * 60;
return formatTime(minute) + ":" + formatTime(second);
};
const formatTime = (time) => {
return ("0" + time).slice(-2);
};
let sec = seconds;
useEffect(() => {
// let timer = null;
if (!start) {
let sec = seconds;
funRef.current = setInterval(() => {
console.log("Seconds remaining:", sec);
if (sec <= 0) {
clearInterval(funRef.current);
setStartToggle(true);
}
setSeconds(sec--);
}, 1000);
} else {
clearInterval(funRef.current);
}
}, [start]);
const startTimer = () => {
setSeconds(sec);
setStartToggle(false);
};
const stopTimer = () => {
setSeconds(sec);
setStartToggle(true);
};
return (
<View style={styles.container}>
<StatusBar barStyle="light-content" />
<Text
style={
seconds ? styles.timerText : [styles.timerText, { color: "red" }]
}
>
{getRemaining(seconds)}
</Text>
{start ? (
<TouchableOpacity onPress={startTimer} style={styles.button}>
<Text style={styles.buttonText}>Start</Text>
</TouchableOpacity>
) : (
<TouchableOpacity
onPress={stopTimer}
style={[styles.button, { borderColor: "orange" }]}
>
<Text style={styles.buttonText}>Stop</Text>
</TouchableOpacity>
)}
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#07121B",
alignItems: "center",
justifyContent: "center",
},
button: {
borderWidth: 10,
borderColor: "#89aaff",
width: screen.width / 2,
height: screen.width / 2,
borderRadius: screen.width / 2,
justifyContent: "center",
alignItems: "center",
},
buttonText: {
fontSize: 40,
color: "#89aaff",
fontWeight: "bold",
},
timerText: {
fontSize: 90,
color: "#89aaff",
fontWeight: "bold",
marginBottom: 20,
},
});
回答1:
Currently you use the useState
hook to store the interval reference. At every re-render of the App
component the fun
state is reset, but will not be the exact same reference to the interval.
Instead use the useRef
hook. It can create a reference to the interval which will not change during re-renders. This means that the value in the current
property of the reference will always be the exact same one.
To top it off, use the useEffect
hook to watch when a running state is set or unset and start and stop the timer based on that state.
import React, { useState, useRef, useEffect } from 'react'
export default function App() {
const [isRunning, setIsRunning] = useState(false);
const funRef = useRef(null);
const startTimer = () => {
if (!isRunning) {
setIsRunning(true);
}
};
const stopTimer = () {
if (isRunning && funRef.current !== null) {
setIsRunning(false);
}
};
useEffect(() => {
if (isRunning) {
funRef.current = setInterval(() => { // Save reference to interval.
// ...
}, 1000);
} else {
clearInterval(funRef.current); // Stop the interval.
}
}, [isRunning]);
// ...
}
回答2:
Add interval timer inside useEffect block, and run this block every time start changes:
useEffect(()=> {
let timer = null;
if(!start) {
let sec = seconds;
timer = setInterval(() => {
console.log("akak:", sec);
if (sec <= 0) clearInterval(timer);
setSeconds(sec--);
}, 1000);
} else {
clearInterval(timer);
}
}, [start]);
const startTimer = () => {
setStartToggle(false);
};
const stopTimer = () => {
setStartToggle(true);
};
Also, I recommend you to use the exact remaining time. Javascript is a single threaded language, and this interval could be more than one second
EDIT: using the exact time.
useEffect(() => {
let timer = null;
if (!start) {
let sec = seconds;
const startTime = Date.now();
timer = setInterval(() => {
const currentTime = Date.now();
sec = sec - Math.floor((currentTime - startTime) / 1000);
if (sec < 0) {
clearInterval(timer);
setSeconds(0);
} else {
setSeconds(sec);
}
}, 1000);
}
}, [start]);
来源:https://stackoverflow.com/questions/64371873/unable-to-clear-setinterval-in-react-native-app