How to clear the interval in React - javascript

I am building a stopwatch by using useState hooks in react.js but while implementing pause functionality I have noticed that I am not able to clear the interval. I am new and I have tried many things but still it doesn't work. If anyone can help me fix the code or suggest me other way.
Here is my code:
function App() {
const [stopwatch, setStopwatch] = useState({
hour: 0,
min: 0,
sec: 0,
secElapsed: 0,
minElapsed: 0,
});
const [buttonState, setButtonState] = useState({
start: true,
stop: false,
pause: false,
resume: false,
});
var interval = null;
function onStart() {
// i want to clear this interval when the onPause function is called
var clrInt = setInterval(() => {
setStopwatch(prevValue => {
prevValue.secElapsed++;
return {
hour: Math.floor(prevValue.secElapsed / 3600),
minElapsed: Math.floor((prevValue.secElapsed + 1) / 60),
min: prevValue.minElapsed % 60,
sec: prevValue.secElapsed % 60,
secElapsed: prevValue.secElapsed,
};
});
}, 1000);
setButtonState(prevValue => {
return {
...prevValue,
start: false,
pause: true,
stop: true,
};
});
interval = clrInt;
}
function onPause() {
setButtonState(prevValue => {
return {
...prevValue,
pause: false,
resume: true,
};
});
// i want to clear the interval in onStart function here
clearInterval(interval);
}
return (
<div>
<h1>
{stopwatch.hour < 10 ? '0' + stopwatch.hour : stopwatch.hour}:
{stopwatch.min < 10 ? '0' + stopwatch.min : stopwatch.min}:
{stopwatch.sec < 10 ? '0' + stopwatch.sec : stopwatch.sec}
</h1>
{buttonState.start ? <button onClick={onStart}>Start</button> : null}
{buttonState.pause ? <button onClick={onPause}>Pause</button> : null}
{buttonState.stop ? <button>Stop</button> : null}
{buttonState.resume ? <button>Resume</button> : null}
</div>
);
}

Declaring a variable inside the function component will run every time the component is rendered. You need to capture the interval in a useState like
var [intervalState, setIntervalState] = useState(null)
...
function onStart() {
var clearInterval = setInterval(() => {
...
})
setIntervalState(clearInterval)
}
This will make sure your clearInterval value will persist across rerenders

I don't think the interval fits into the component's state. Instead, I would use the useRef hook to keep it through the component's lifetime.
Anyways, it's good that you start thinking in hooks :)
A cleaner way is to implement something like useInterval:
const [isRunning, setIsRunning] = useState(true)
useInterval(() => {
// Your custom logic here
}, isRunning ? 1000 : null)
const onPause = () => {
// ...
setIsRunning(false)
}
Here's a demo with an actual implementation of the hook.

Related

Does React rendering effects the setTimeout or setInterval function wrapped in a useRef and useRef is good way to achieve pause, stop, update question

I'm trying to achieve the slider functionality, where I want to slider to move to the next question after every three seconds [this part is working fine]
Features i'm trying to achieve:
OnMouseEnter: Timer stops, css animation stops(working)
OnMouseleave: Start the timer from remaining time left
OnClick: Change Question number to the selected items (working) and start animation
The issue is when I changed the question_number by onClick event, the next question start showing after every 1 seconds instead of 3 seconds. I think the problem is somehow with the React Re-rendering.
setSlider is simple useState has object:
const [timeSlider, setTimeSlider] = useState({
question_length: 4,
question_number: 0,
current_time: 0
});
I'm using useEffect to check the viewport width so react Re-render the components everytime, Viewport changes
I tried to store the Interval for timer in a react useRef. but still not working. Here is my code:
I use useRef, so I can Resume, Pause and updateNumber outSide where the first time setInterval started
const interval = useRef(null);
After EverySecond update the useState: Seconds
useEffect(() => {
setRemaining({
start_time: Date.now(),
stop_time: 0
});
interval.current = setInterval(() => {
setSeconds(seconds => seconds + 1);
}, 1000);
pauseSlider();
return () => clearInterval(interval);
}, []);
And when ever the seconds state changed, I make sure after every three seconds. question_number increased only when its less than 3
useEffect(() => {
if (seconds === 3) {
setSeconds(seconds => 0);
setTimeSlider(prev => prev.question_number === 3 ? { ...timeSlider,
question_number: 0
} : { ...timeSlider,
question_number: timeSlider.question_number + 1
});
}
}, [seconds]);
Below are the pause, resume and update function.
function pauseSlider() {
setSliderHover(true)
setRemaining({
start_time: 0,
stop_time: Date.now()
});
clearInterval(interval.current);
}
function resumeSlider() {
setSliderHover(false)
setRemaining(prev => ({ ...prev,
start_time: prev.stop_time,
stop_time: 0
}));
interval.current = setInterval(() => {
setSeconds(seconds => seconds + 1);
}, 1000);
}
function updateNumber(num) {
setSliderHover(false)
setSeconds(0)
setRemaining({
start_time: 0,
stop_time: 0
});
clearInterval(interval.current);
interval.current = setInterval(() => {
setSeconds(seconds => seconds + 1);
}, 1000);
setTimeSlider({ ...timeSlider,
question_number: num
});
}
The Problem could be in these functions, I tried to use setTimeout instead of setInterval same results.
Below is the picture of slider, so you have more clear idea what i'm trying to achieve.
Instead of using the JS, I used the GSAP for this here is my solution for it
useLayoutEffect(() => {
const ctx = gsap.context((self) => {
TimelineRef.current = gsap.timeline({
defaults: {
repeat: -1,
ease: "none",
}
})
.to(".timerSlier_left",
{
x: "0%",
duration: 10,
onRepeat: () => UpdateToNextQuestion(timeSlider),
}
)
}, ContainerRef); // <- Scope!
return () => ctx.revert(); // <- Cleanup!
}, []);
const [ remaining, setRemaining ] = useState(0);
useEffect(()=>{
if ( isInViewport ){
resumeSlider()
}else{
pauseSlider()
}
}, [ isInViewport ]);
const UpdateToNextQuestion = () => {
settimeSlider( prev => prev.question_number === 3 ? ({...prev, question_number: 0}) : ({...prev, question_number: prev.question_number+1}))
}
function pauseSlider(){
console.log("stop slider")
setSliderHover(true)
TimelineRef.current.pause();
}
function resumeSlider(){
setSliderHover(false)
TimelineRef.current.resume();
}
function updateNumber(num){
TimelineRef.current.restart();
settimeSlider( timeSlider => ({ ...timeSlider, question_number: num }) );
console.log("RESUMING")
setSliderHover(false)
TimelineRef.current.resume();
}
I'm handing timeout functionality from GSAP timeline.

How to update an object array every second in state?

I'm trying to track the time for each todo in my app by saving it in an array of objects. I managed to make it work, but since I'm updating the state only when I stop the timer, the component is not displaying each second passing.
What would be the simplest way to update the state each second? I've tried for a few days (mainly with setInterval()) but I still can't make it work since it's a little beyond my knowledge.
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
// todos array of objects
todos: null,
// value to hold temp text input
value: '',
};
...
}
// start&stop timer + updating counters
startTimer(todoKey) {
const newTodos = this.state.todos.map(element => {
if (element.key === todoKey) {
if (element.status === "stopped") {
return { ...element, status: 'started', startTime: new Date()};
} else {
let sessionSpent = new Date() - element.startTime;
let progress = element.goal !== null ? ((element.spent === null ? sessionSpent : element.spent + sessionSpent) * 1 / element.goal) : 1;
return { ...element, status: 'stopped', startTime: null, progress: progress, spent: element.spent === null ? sessionSpent : element.spent + sessionSpent };
}
} else { return element; }
});
this.setState({
todos: newTodos
});
}
....
After some more trials, I managed to make it work.
I will leave it here if anyone is having the same problem, but be aware: I'm just starting, so it's possible that I'm doing a surgery with a hammer.
If that's the case, please suggest a better solution. Thank you.
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
//todos array of objects
todos: null,
//value to hold temp text input
value: '',
//timer status: need to save it outside of the setInterval() loop so it doesn't stop after 1 iteration.
timerStatus: false,
//setInterval function container
interval: null,
// active timer todoKey: saving it here so I can avoid that a timer is stopped by pressing another timer's button.
activeTimer: null
};
...
}
// start&stop timer + updating counters
startTimer(todoKey) {
// first I check that there's no active timer
if (!this.state.timerStatus) {
// if no timer is active, I save the active timer's key in the state(so I can forbid activities on the timer from another timer's button)
this.setState({ activeTimer: todoKey });
// here I start the setInterval() function in a variable and save it into a variable (so later I can save it in the state)
let interval = setInterval(() => {
// looping through the array
const newTodos = this.state.todos.map(element => {
// finding the right object in the array so I can save the progress and the time spent, each second
if (element.key === todoKey) {
let sessionSpent = new Date() - element.startTime;
let progress = element.goal !== null ? ((element.spent === null ? sessionSpent : element.spent + sessionSpent) * 1 / element.goal) : 1;
return { ...element, status: 'started', startTime: new Date(), spent: element.spent + 1000, progress: progress }
} else {
return element;
}
});
this.setState({
todos: newTodos
});
}, 1000);
// setting the timerStatus as true(so I can have a trouth source outside of the loop when I need to stop it)
// saving the variable containint setInterval() in the state (so I can use clearInterval() from the else block
this.setState({ timerStatus: true, interval: interval });
console.log(this.state.timerStatus);
} else {
// checking if the pressed button is the one belonging to the active timer
if (todoKey === this.state.activeTimer) {
// saving in the state the timer's status (thus making possible for another timer to start
// clearing the setInterval() function
this.setState({ timerStatus: false, interval: clearInterval(this.state.interval) });
// looping through the array to save the current progress
const newTodos = this.state.todos.map(element => {
if (element.key === todoKey) {
let sessionSpent = new Date() - element.startTime;
let progress = element.goal !== null ? ((element.spent === null ? sessionSpent : element.spent + sessionSpent) * 1 / element.goal) : 1;
return { ...element, status: 'stopped', startTime: null, progress: progress, spent: element.spent === null ? sessionSpent : element.spent + sessionSpent };
} else {
return element;
}
}
);
// updating the state
this.setState({
todos: newTodos
});
} else {
// showing an error Toast when user tries to start another timer if there's one already active (when the check (todoKey === this.state.activeTimer) is false)
Toast.show("A timer is already running and multitasking is bad D:", Toast.LONG);
}
}
}
Here is a simple example:
this.setInterval( () => {
let d = new Date();
let result = d.getHours() + d.getMinutes() / MINUTES_IN_HOUR;
this.setState({
timeLineTop: result
})
}, 1000);

setTimeout inside useEffect with useState

I have array of objects inside my useState hook, and I would like to change one property of each array element let's say it looks like:
const array = [{id:1, isDisplayed: false}, {id:2, isDisplayed: false}, {id:3, isDisplayed: true}]
and while Im trying to use setTimeout inside useEffect hook to change property displayed everywhere where it's not true to isDisplayed: true , it waits for the dedicated time, and changes everything at once, what I want to achieve is to change each element with its own delay. I mean something like
const DELAY = 2000 and inside setTimeout for instance setTimeout(() => ... , DELAY * id)
because in place when I render jsx, everything appears all at once, and i just want to make small delays between appearing each element. For instance, first element appears after 2s, second after 3s (not 3s after first one)
My current code looks like:
React.useEffect(() => {
setTimeout(() => {
setArray(array.map((item)=> !item.isDisplayed ? {...item, displayed: true} : item))
}, DELAY * Math.floor(Math.random() * 5);
}, [])
You can set a timeout and trigger an update, if some items are not visible.
And track if new items are revealed with revealed and if no new items were revealed, stop the flow.
function TodoApp() {
const [items, setItems] = React.useState([
{ id: 1, isDisplayed: false },
{ id: 2, isDisplayed: false },
{ id: 3, isDisplayed: false },
]);
React.useEffect(() => {
let currentTimeout = null;
const displayMoreItems = () => {
setItems(prevItems => {
let revealed = false;
const nextItems = prevItems.map(item => {
if (!revealed && !item.isDisplayed) {
revealed = true;
return { ...item, isDisplayed: true };
}
return item;
});
if (revealed) {
currentTimeout = setTimeout(() => {
displayMoreItems();
}, 1000);
}
return nextItems;
});
};
currentTimeout = setTimeout(() => {
displayMoreItems();
}, 1000);
return () => {
if (currentTimeout) {
clearTimeout(currentTimeout);
}
};
}, [setItems]);
return <div>{items.map(item => (item.isDisplayed ? item.id : null))}</div>;
}
ReactDOM.render(<TodoApp />, document.querySelector('#app'));
Here is a fiddle
const DELAY = 2000;
React.useEffect(() => {
let count = 1;
array.forEach((item) => {
if (!item.displayed) {
setTimeout(() => {
item.displayed = true;
setArray([...array]);
}, DELAY * count);
count ++;
}
})
}, [])

Display currentTime of a song in vue

I'd like to know how can I make time constantly update itself. So when I press the play button the seconds start to update automatically from 0:00 to the end , because now it just updates onclick. I am trying to use HTML5 audio and I have successfully managed to get the time updating as a label from this line of code:
sound.ontimeupdate = function () { document.getElementById('Time').innerHTML = sound.currentTime.toFixed() }
But thats not what I need, I would like to get the time attribute in data() to get updated and displayed in the label as shown in my HTML code.
I tried adding an event listener but it did not work... It gets called and every call was logged with console.log but time attribute was not updated
let sound = null
export default {
data () {
return {
isPlaying: false,
time: 0,
duration: 0
}
},
methods: {
playMusic () {
if (!sound) {
sound = new Audio(require('assets/YES.mp3'))
}
this.isPlaying = true
sound.play()
// sound.addEventListener('timeupdate', function () { this.time = sound.currentTime.toFixed() }) -- did not work
this.time = sound.currentTime.toFixed()
}
Html:
<label id="Time" #timeupdate>
{ { time } }:{ { duration } }
</label>
Inside your addEventListener you get a different this than you might expect.
Either use fat arrow
sound.addEventListener('timeupdate', () => this.time = sound.currentTime.toFixed() )
or, the old way, save this
let that = this
sound.addEventListener('timeupdate', function () { that.time = sound.currentTime.toFixed() })
you could just add a generic timer dynamically. You can use a watch to add/remove it like so:
(untested code)
export default {
data() {
return {
isPlaying: false,
time: 0,
duration: 0,
intervalId: null,
sound: null
};
},
watch: {
isPlaying(isPlaying) {
if (this.intervalId !== null) {
clearInterval(this.intervalId);
}
if (isPlaying) {
this.sound.play();
this.intervalId = setInterval(() => {
this.time = this.sound.currentTime.toFixed();
}, 500);
} else {
this.sound.stop();
}
}
},
methods: {
playMusic() {
if (!this.sound) {
this.sound = new Audio(require("assets/YES.mp3"));
}
this.isPlaying = true;
}
}
};

Cannot read property 'commit' of undefined VUEX

Please help me it always says Cannot read property 'commit' of undefined. Here is my code in store.js.
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
export const store = new Vuex.Store({
state:{
timer: null,
totaltime: (25 * 60)
},
mutations:{
startTimer: (state, context) => {
//here is the problem part I think
state.timer = setInterval(() => context.commit('countDown'),
1000)
},
countDown: state => {
var time = state.totaltime
if(time >= 1){
time--
}else{
time = 0
}
},
stopTimer: state => {
clearInterval(state.timer)
state.timer = null
},
resetTimer: state => {
state.totaltime = (25 * 60)
clearInterval(state.timer)
}
},
getters:{
minutes: state => {
const minutes = Math.floor(state.totaltime / 60);
return minutes;
},
seconds: (state, getters) => {
const seconds = state.totaltime - (getters.minutes * 60);
return seconds;
}
},
actions:{
}
})
I have problem it debugging. it always says like this
'Cannot read property 'commit' of undefined'
Here is my Timer.vue code for calling
methods: {
formTime(time){
return (time < 10 ? '0' : '') + time;
},
startTimer(){
this.resetButton = true
this.$store.commit('startTimer')
},
stopTimer(){
this.$store.commit('stopTimer')
},
resetTimer(){
this.$store.commit('resetTimer')
},
},
computed: {
minutes(){
var minutes = this.$store.getters.minutes;
return this.formTime(minutes)
},
seconds(){
var seconds = this.$store.getters.seconds;
return this.formTime(seconds);
},
timer: {
get(){
return this.$store.state.timer
}
}
}
My code in Timer.vue script computed and methods. I cannot track where the problem is... Please help me Im stuck with this here.
Mutations do not have access to any context. They are meant to be atomic, that is they work directly with one facet of state. You should make your startTimer an action that commits the timer and then starts the countdown
mutations: {
// add this one for setting the timer
setTimer (state, timer) {
state.timer = timer
}
},
actions: {
startTimer ({ commit }) {
commit('stopTimer') // just a guess but you might need this
commit('setTimer', setInterval(() => {
commit('countDown')
}, 1000))
}
}
This would need to be called via dispatch instead of commit
this.$store.dispatch('startTimer')

Categories

Resources