29

I want to countdown from 3 to 1 when a screen is loaded in react-native. I tried it with setTimeOut like this and it didn't work. What am I doing wrong here? How can I achieve this? When the screen is loaded, I want to show 3 =-> 2 ==> 1 with 1 second interval. Here is my code.

 constructor(props) {
        super(props);

        this.state = {
            timer: 3
        }
    }

    // componentDidMount 
    componentDidMount() {
        setTimeout(() => {
            this.setState({
                timer: --this.state.timer
            })
        }, 1000);
    }
Shashika Virajh
  • 8,497
  • 17
  • 59
  • 103

6 Answers6

42

In your code setTimeout is called in componentDidMount and ComponetDidMount will be called once in whole component lifeCycle. So, the function within setTimeout will be called once only. i.e. just after the first render but upon successive render, the componentDidMount won't be called.

Solution to your problem can be:

1. Class Component

constructor(props: Object) {
  super(props);
  this.state ={ timer: 3}
}

componentDidMount(){
  this.interval = setInterval(
    () => this.setState((prevState)=> ({ timer: prevState.timer - 1 })),
    1000
  );
}

componentDidUpdate(){
  if(this.state.timer === 1){ 
    clearInterval(this.interval);
  }
}

componentWillUnmount(){
 clearInterval(this.interval);
}

render() {
  return (
    <View style={{ flex: 1, justifyContent: 'center', }}>
      <Text> {this.state.timer} </Text>
    </View> 
  )
}

'setInterval' vs 'setTimeout'

Advantage of using a function in setState instead of an object

memory leak because of setInterval: if we unmount the component before clearInterval called, there is a memory leak because the interval that is set when we start and the timer is not stopped. React provides the componentWillUnmount lifecycle method as an opportunity to clear anything that needs to be cleared when the component is unmounted or removed.

2. Functional Component

function CountDownTimer(props) {
  const [time, setTime] = React.useState(props.initialValue || 10);
  const timerRef = React.useRef(time);

  React.useEffect(() => {
    const timerId = setInterval(() => {
      timerRef.current -= 1;
      if (timerRef.current < 0) {
        clearInterval(timerId);
      } else {
        setTime(timerRef.current);
      }
    }, 1000);
    return () => {
      clearInterval(timerId);
    };
  }, []);

  return ( 
    <View style={{ flex: 1, justifyContent: 'center' }}>
      <Text> {time} </Text>
    </View>
  )
}

useRef returns a mutable ref object whose .current property is initialized to the passed argument (initialValue). The returned object will persist for the full lifetime of the component. So on component rerender the object reference will be same.

Answers given by @TheEhsanSarshar and @Rishabh Jain will also work. I have shown a slightly different solution from the others.

Mohammed Ashfaq
  • 3,353
  • 2
  • 15
  • 22
  • 2
    If you're going to update state with current state you're better off using : `this.setState((prevState) => { return { timer: prevState.timer-- } }) ` – fv_dev Sep 03 '18 at 05:33
  • 2
    @fv_dev is there any advantage in using this method. – Mohammed Ashfaq Sep 03 '18 at 05:44
  • @MohammedAshfaq They say that the current state cannot be reliably gotten with `this.state` whereas using `prevState` it is. I have yet to be convinced with an example. – Joshua Pinter Aug 03 '19 at 22:44
15

Updated Hooks (using useEffect) version to countdown using setInterval in react-native:

const [timerCount, setTimer] = useState(60)

useEffect(() => {
  let interval = setInterval(() => {
    setTimer(lastTimerCount => {
        lastTimerCount <= 1 && clearInterval(interval)
        return lastTimerCount - 1
    })
  }, 1000) //each count lasts for a second
  //cleanup the interval on complete
  return () => clearInterval(interval)
}, []);

use the state variable timerCount as: <Text>{timerCount}</Text>

Rishabh Jain
  • 1,341
  • 2
  • 8
  • 8
  • can you explain what `lastTimerCount <= 1 && clearInterval(interval)` is doing? – Tee Dec 23 '21 at 04:33
  • @Tee This clears the interval once it remains under 1 second and has finished so to say. `clearInterval` is an inbuilt function in the browser window or NodeJS to clear the interval so that it isn't executed any longer. Similarly, there is clearTimeout for setTimeout. – alexanderdavide Jan 20 '22 at 08:50
  • 1
    @alexanderdavide Thanks for the response. Intuitively, I figured that's what was happening. Just the syntax of the expression is a little unexpected -- I was looking for some logic like `if (lastTimerCount <= 1) {clearInterval(interval)}` ... The `&&` between a conditional expression and a method is the part I need to wrap my head around. – Tee Jan 26 '22 at 16:44
4

If anyone wants to start the timer again on a button press, this will be the code in react-hooks:

let timer = () => {};

const myTimer = () => {
    const [timeLeft, setTimeLeft] = useState(30);

    const startTimer = () => {
        timer = setTimeout(() => {
            if(timeLeft <= 0){
                clearTimeout(timer);
                return false;
            }
         setTimeLeft(timeLeft-1);
        }, 1000)
     }

     useEffect(() => {
         startTimer();
         return () => clearTimeout(timer);
     });    

    const start = () => {
        setTimeLeft(30);
        clearTimeout(timer);
        startTimer();
    }

    return (
       <View style={styles.container}>
          <Text style={styles.timer}>{timeLeft}</Text>
          <Button onPress={start} title="Press"/>
    </View>
)}

Here in this example, I have taken a timer of 30 seconds

Ritik Jain
  • 143
  • 1
  • 6
4

Usage:

timestamp prop must be in seconds

 const refTimer = useRef();
  
  const timerCallbackFunc = timerFlag => {
    // Setting timer flag to finished
    console.warn(
      'You can alert the user by letting him know that Timer is out.',
    );
  };
    
    
 <Timer
 ref={refTimer}
 timestamp={moment(item?.time_left).diff(moment(), 'seconds')}
 timerCallback={timerCallbackFunc}
 textStyle={styles.timerTextAHL}
 />

Timer.js

import React, {
  useState,
  useEffect,
  useRef,
  forwardRef,
  useImperativeHandle,
} from 'react';
import { Text, View } from 'react-native';

const Timer = forwardRef((props, ref) => {
  // For Total seconds
  const [timeStamp, setTimeStamp] = useState(
    props.timestamp ? props.timestamp : 0,
  );
  // Delay Required
  const [delay, setDelay] = useState(props.delay ? props.delay : 1000);

  // Flag for informing parent component when timer is over
  const [sendOnce, setSendOnce] = useState(true);

  // Flag for final display time format
  const [finalDisplayTime, setFinalDisplayTime] = useState('');

  useInterval(() => {
    if (timeStamp > 0) {
      setTimeStamp(timeStamp - 1);
    } else if (sendOnce) {
      if (props.timerCallback) {
        props.timerCallback(true);
      } else {
        console.log('Please pass a callback function...');
      }
      setSendOnce(false);
    }
    setFinalDisplayTime(secondsToDhms(timeStamp));
  }, delay);

  function secondsToDhms(seconds) {
    seconds = Number(seconds);
    var d = Math.floor(seconds / (3600 * 24));
    var h = Math.floor((seconds % (3600 * 24)) / 3600);
    var m = Math.floor((seconds % 3600) / 60);
    var s = Math.floor(seconds % 60);

    var dDisplay = d > 0 ? d + 'd ' : '';
    var hDisplay = h > 0 ? h + 'h ' : '';
    var mDisplay = m > 0 ? m + 'm ' : '';
    var sDisplay = s > 0 ? s + 's ' : '';
    return dDisplay + hDisplay + mDisplay + sDisplay;
  }

  const refTimer = useRef();
  useImperativeHandle(ref, () => ({
    resetTimer: () => {
      // Clearing days, hours, minutes and seconds
      // Clearing Timestamp
      setTimeStamp(props.timestamp);
      setSendOnce(true);
    },
  }));

  return (
    <View ref={refTimer} style={props.containerStyle}>
      <Text style={props.textStyle}>{sendOnce ? finalDisplayTime : '0'}</Text>
    </View>
  );
});

function useInterval(callback, delay) {
  const savedCallback = useRef();

  // Remember the latest function.
  useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);

  // Set up the interval.
  useEffect(() => {
    function tick() {
      savedCallback.current();
    }
    if (delay !== null) {
      const id = setInterval(tick, delay);
      return () => {
        clearInterval(id);
      };
    }
  }, [delay]);
}

export default Timer;

enter image description here

Ajmal Hasan
  • 885
  • 11
  • 9
3

The hooks version.

function CountDown() {
   const [count, setCount] = useState(3)

   useEffect(() => 
     let interval = setInterVal(() => {
        setCount(prev => {
           if(prev === 1) clearInterval(interval)
           return prev - 1
        })
     })
     // interval cleanup on component unmount
     return () => clearInterval(interval)
   ), [])

   return <Text>{count}</Text>
}
TheEhsanSarshar
  • 2,677
  • 22
  • 41
0

Code Of Power Hope so this Way is Easy

  componentDidMount() {
        this.CounterInterval()
  }
  CounterInterval = () => {
    this.interval = setInterval(
      () =>   this.setState({
        timer: this.state.timer - 1
      }, () => {
        if (this.state.timer === 0) {
          clearInterval(this.interval);
        }
      }),
      1000
    );
  }
Mirza Hayat
  • 284
  • 5
  • 11