Countdown timer in react-native - react-native

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

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.

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>

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;

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>
}

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

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

Related

Change ad banner for react native application

I have an advertising banner at the bottom of the main page of the application! I want to change it on a timer to another banner!
Some Screen:
function SomeScreen() {
return (
<View>
...
<MyBanner></MyBanner>
</View>
)
}
Banner Component:
const adUnitIds = ['ca-app-pub-eeeeeeee/rrrrrrrr', 'ca-app-pub-ffffff/ggggggggg', 'ca-app-pub-yyyyyyyyy/xxxxxxxxx']
let activeNum = 0
function MyBanner({}) {
const [activeAdUnitId, setActiveAdUnitId] = useState(adUnitIds[0])
useEffect( () => {
const interval = setInterval( () => {
activeNum = activeNum === adUnitIds.length - 1 ? 0 : activeNum + 1
setActiveAdUnitId(adUnitIds[activeNum])
}, 20000)
return () => {
clearInterval(interval)
}
}, [])
return (
<View>
<BannerAd
unitId={activeAdUnitId}
size={BannerAdSize.ANCHORED_ADAPTIVE_BANNER }
requestOptions={{
requestNonPersonalizedAdsOnly: true
}}
/>
</View>
)
}
It works, but would like to know about best practices! How good is this solution?

React Native sound on interval skipping

I'm trying to get React Native to whistle 3 times before a timer, so for example, whistle 3 seconds in a row, then let the timer go, then whistle again, but for some reason it is only doing it twice, it's skipping the middle whistle and sometimes the last one.
I've tried mounting the sound before hand, reducing the sound duration to about .3 seconds, and it is still skipping some plays. I know I need to do some refactor on the timers, but I think at least playing the sound should work.
import {
View,
Text,
StyleSheet,
TouchableOpacity,
Dimensions,
Vibration,
} from "react-native";
import React from "react";
import { StatusBar } from "expo-status-bar";
import { Audio } from "expo-av";
const screen = Dimensions.get("window");
let timeout: NodeJS.Timeout | undefined = undefined;
interface TimerComponentProps {
timeInSeconds?: number;
}
export const TimerComponent: React.FC<TimerComponentProps> = ({
timeInSeconds = 5,
}) => {
const [remaningSeconds, setRemainingSeconds] = React.useState(timeInSeconds);
const [isActive, setIsActive] = React.useState(false);
const [sound, setSound] = React.useState<Audio.Sound | undefined>(undefined);
const [shouldCount, setShouldCount] = React.useState(false);
const [counter, setCounter] = React.useState(3);
const { minutes, seconds } = React.useMemo(() => {
const minutes = Math.floor(remaningSeconds / 60);
const seconds = remaningSeconds % 60;
return { minutes, seconds };
}, [remaningSeconds]);
async function mountSound() {
try {
const { sound } = await Audio.Sound.createAsync(
require("../../assets/audio/Whistle.wav")
);
setSound(sound);
} catch (error) {
console.error(error);
}
}
async function playWhistle() {
if (sound) {
try {
await sound.playAsync();
} catch (error) {
console.error(error);
}
}
}
const endTimer = async () => {
try {
await playWhistle();
setIsActive(false);
} catch (error) {
console.error(error);
}
};
const startCounter = async () => {
await mountSound();
setShouldCount(true);
};
const resetTimer = () => {
if (timeout) {
clearTimeout(timeout);
} else {
timeout = setTimeout(() => {
setRemainingSeconds(timeInSeconds);
clearTimeout(timeout);
}, 1000);
}
};
React.useEffect(() => {
let counterInterval: NodeJS.Timer | undefined = undefined;
if (shouldCount) {
counterInterval = setInterval(() => {
try {
if (counter === 1) {
setCounter((counter) => counter - 1);
}
if (counter > 1) {
playWhistle();
Vibration.vibrate();
setCounter((counter) => counter - 1);
} else {
// Plays the whistle sound and vibrates the device
playWhistle();
Vibration.vibrate();
// Restarts the counter
setCounter(3);
setShouldCount(false);
// Starts the timer
setIsActive(true);
// Stops the counter
clearInterval(counterInterval);
}
} catch (error) {
console.error(error);
}
}, 1000);
} else if (!shouldCount && counter !== 0) {
clearInterval(counterInterval);
}
return () => clearInterval(counterInterval);
}, [shouldCount, counter]);
React.useEffect(() => {
let timerInterval: NodeJS.Timer | undefined = undefined;
if (isActive) {
timerInterval = setInterval(() => {
if (remaningSeconds === 1) {
setRemainingSeconds((remaningSeconds) => remaningSeconds - 1);
}
if (remaningSeconds > 1) {
setRemainingSeconds((remaningSeconds) => remaningSeconds - 1);
} else {
Vibration.vibrate();
endTimer();
resetTimer();
}
}, 1000);
} else if (!isActive && remaningSeconds === 0) {
resetTimer();
clearInterval(timerInterval);
}
return () => clearInterval(timerInterval);
}, [isActive, remaningSeconds]);
React.useEffect(() => {
return sound
? () => {
sound.unloadAsync();
setSound(undefined);
}
: undefined;
}, [sound]);
const parseTime = (time: number) => {
return time < 10 ? `0${time}` : time;
};
return (
<View style={styles.container}>
<StatusBar style="light" />
<Text style={styles.timerText}>{`${parseTime(minutes)}:${parseTime(
seconds
)}`}</Text>
<TouchableOpacity onPress={startCounter} style={styles.button}>
<Text style={styles.buttonText}>{isActive ? "Pause" : "Start"}</Text>
</TouchableOpacity>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#07121B",
alignItems: "center",
justifyContent: "center",
},
button: {
borderWidth: 10,
borderColor: "#B9AAFF",
width: screen.width / 2,
height: screen.width / 2,
borderRadius: screen.width / 2,
alignItems: "center",
justifyContent: "center",
},
buttonText: {
color: "#B9AAFF",
fontSize: 20,
},
timerText: {
color: "#fff",
fontSize: 90,
},
});
The issue was that expo-av leaves the audio file at it's end, so the next time you play it, nothing will sound because the file is already over, the way to fix it is pretty simple:
async function playWhistle() {
if (sound) {
try {
await sound.playAsync();
sound.setPositionAsync(0); // ADD THIS LINE
} catch (error) {
console.error(error);
}
}
}

After a button is pressed to start a setInterval(), how do I clearInterval once a condition is met

I have an app that starts an interval when a button is pressed.
I want to stop the interval after a state reaches 5.
I have tried adding the if condition in UseEffect, and tried putting the condition within the function itself, but both don't seem to work. The console.log(sequence) does print successfully in useEffect I can see that the sequence does indeed increase, but it keeps increasing beyond 5 and never clearInterval.
const [sequence, setSequence] = useState(0)
const interval = () =>{
setInterval(() => {
setSequence((prevSequence) => (
prevSequence+1
))
}, 2000);
}
const plant = ()=>{
interval()
console.log(sequence)
if(sequence>5){
clearInterval (interval)
}
}
useEffect(() => {
console.log(sequence)
if(sequence>5){
return () => clearInterval(interval);
}
}, [sequence]);
return(
<View style = {styles.container} >
<Image
style={{height:500,width:300,}}
source= {tomato[sequence]}
/>
<Button
title = 'Fertilize Plant'
style= {styles.text}
onPress= {plant}>
</Button>
</View>)
}
Issue
You are not clearing the interval correctly.
const interval = () => {
setInterval(() => {
setSequence((prevSequence) => prevSequence + 1)
}, 2000);
};
...
useEffect(() => {
console.log(sequence);
if (sequence > 5) {
return () => clearInterval(interval);
}
}, [sequence]);
Here interval is a reference to the function, not the actual interval timer id returned from setInterval.
Solution
Store the interval timer id in a React ref to be referenced around the component.
const intervalRef = useRef();
const interval = () => {
intervalRef.current = setInterval(() => {
setSequence((prevSequence) => prevSequence + 1)
}, 2000);
};
...
useEffect(() => {
const intervalId = intervalRef.current;
// also clear on component unmount
return () => clearInterval(intervalId);
}, []);
useEffect(() => {
if (sequence > 5) {
clearInterval(intervalRef.current);
}
}, [sequence]);
Also, there's no need to check the sequence value and clear the interval in the plant callback, the useEffect hook with dependency on sequence will handle that. Plant only needs to start the interval.
const plant = () => {
interval();
};
You can use the clearInterval function to stop the interval you had set.
Here is an example;
let counter = 0;
const interval = setInterval(() => {
counter++;
console.log(`Counter = ${counter}`);
if (counter >= 3) {
console.log("Interval Stopped");
clearInterval(interval);
}
}, 1000);

How to make progress bar with minute interval?

library used react-native progress
for making an progress bar
import * as Progress from 'react-native-progress';
<Progress.Bar progress={0.3} width={200} />
use this library react-native progress
initial value set to 0
const [progressBarValue, setProgressBarValue] = useState(0)
useEffect(() => {
const intervalId = setInterval(() => {
setProgressBarValue((prev) => {
if (prev >= 1.2) {
setCongratulations('Congratulations')
clearInterval(intervalId);
return 1.120;
} else {
return prev + 0.01;
}
});
}, 1000);
return () => clearInterval(intervalId);
}, []);
render code
<Progress.Bar progress={progressBarValue} width={width-50} color={'rgba(221,196,145, 1)'}/>

Update position of All places in react-native-sortable-listview

I am using react-native-sortable-listview in react-native for sorting same places.
constructor() {
this.state = {
makers: [
{ kolkata: 'Hawrah Birdge' },
{ Delhi: 'Lal Kila' },
{ Agra: 'Taj Mahal' },
{ Mumbai: 'India Gate' },
],
allObj: {},
order: []
};
}
componentDidMount() {
const newAllObj = this.getAllObjFromMaker(this.state.makers);
const newOrder = this.getOrderFromMaker(newAllObj);
this.setState({ allObj: newAllObj, order: newOrder });
}
getAllObjFromMaker(makers) {
const allObj = makers.reduce((result, d) => {
result[`${d.coordinate.latitude}_${d.coordinate.longitude}`] = d;
return result;
}, {});
return allObj;
}
getOrderFromMaker(allObj) {
const order = Object.keys(allObj);
return order;
}
renderOneDraggableMilestone(milestone) {
const i = this.state.makers.indexOf(milestone);
return (
<TouchableOpacity {...this.props.sortHandlers}>
<Text>{i + 1}</Text>
<Text>{milestone.address}</Text>
</TouchableOpacity>
);
}
arrangedMilestoneList(e) {
const arr = this.state.makers;
arr.splice(e.to, 0, arr.splice(e.from, 1)[0]);
const newAllObj = this.getAllObjFromMaker(arr);
const newOrder = this.getOrderFromMaker(newAllObj);
this.setState({ makers: arr, allObj: newAllObj, order: newOrder
});
}
render() {
return (
<SortableListView
data={this.state.allObj}
order={this.state.order}
activeOpacity={0.5}
onRowMoved={e => {
this.arrangedMilestoneList(e);
this.forceUpdate();
}}
renderRow={(row) => this.renderOneDraggableMilestone(row)}
/>
);
}
I want to arrange places and also their position in this.state.makers as I am doing using i in renderOneDraggableMilestone. On renderRow only draggable place are render so only their position is updated. And renderRow is last to excute so forceUpdate is also not working.
How to rerender after executing renderRow. So all position could be updated.
Ok I have find a way to re-render as follow.
<SortableListView
key={this.state.count}
data={this.state.allObj}
order={this.state.order}
activeOpacity={0.5}
onRowMoved={e => {
this.setState({ count: this.state.count + 1 });
this.props.arrangedMilestoneList(e);
console.log('onRowMoved is called');
}}
onMoveEnd={() => console.log('onMoveEnd is fired')}
renderRow={(row, s1, i) => this.renderOneDraggableMilestone(row, s1, i)}
/>
What I am doing is I added a key attribute to SortableListView and update this key on each onRowMoved action. And because of this it causes re-render.