Unable to clear setInterval in react native app

∥☆過路亽.° 提交于 2021-01-28 06:30:18

问题


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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!