Search code examples
javascriptreact-nativesetintervalcountdowntimer

Unable to clear setInterval in react native app


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:

enter image description here

Output being shown even after Stop is hit:

enter image description here

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,
  },
});


Solution

  • 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]);
    
      // ...
    }