setInterval and React hooks produces unexpected results - reactjs

I have the following component defined in my app scaffolded using create-react:
import React, { useState } from 'react';
const Play = props => {
const [currentSecond, setCurrentSecond] = useState(1);
let timer;
const setTimer = () => {
timer = setInterval(() => {
if (currentSecond < props.secondsPerRep) {
setCurrentSecond(() => currentSecond + 1);
}
}, 1000);
}
setTimer();
return (
<div>
<div>
<p>{currentSecond}</p>
</div>
</div>
);
}
export default Play;
And currentSecond is updated every second until it hits the props.secondsPerRep however if I try to start the setInterval from a click handler:
import React, { useState } from 'react';
const Play = props => {
const [currentSecond, setCurrentSecond] = useState(1);
let timer;
const setTimer = () => {
timer = setInterval(() => {
if (currentSecond < props.secondsPerRep) {
setCurrentSecond(() => currentSecond + 1);
}
}, 1000);
}
return (
<div>
<div>
<button onClick={setTimer}>Start</button>
<p>{currentSecond}</p>
</div>
</div>
);
}
export default Play;
Then currentSecond within the setInterval callback always returns to the initial value, i.e. 1.
Any help greeeeeeatly appreciated!

Your problem is this line setCurrentSecond(() => currentSecond + 1); because you are only calling setTimer once, your interval will always be closed over the initial state where currentSecond is 1.
Luckily, you can easily remedy this by accessing the actual current state via the args in the function you pass to setCurrentSecond like setCurrentSecond(actualCurrentSecond => actualCurrentSecond + 1)
Also, you want to be very careful arbitrarily defining intervals in the body of functional components like that because they won't be cleared properly, like if you were to click the button again, it would start another interval and not clear up the previous one.
I'd recommend checking out this blog post because it would answer any questions you have about intervals + hooks: https://overreacted.io/making-setinterval-declarative-with-react-hooks/

https://overreacted.io/making-setinterval-declarative-with-react-hooks/ is a great post to look at and learn more about what's going on. The React useState hook doesn't play nice with setInterval because it only gets the value of the hook in the first render, then keeps reusing that value rather than the updated value from future renders.
In that post, Dan Abramov gives an example custom hook to make intervals work in React that you could use. That would make your code look more like this. Note that we have to change how we trigger the timer to start with another state variable.
const Play = props => {
const [currentSecond, setCurrentSecond] = React.useState(1);
const [isRunning, setIsRunning] = React.useState(false);
useInterval(() => {
if (currentSecond < props.secondsPerRep) {
setCurrentSecond(currentSecond + 1);
}
}, isRunning ? 1000 : null);
return (
<div>
<div>
<button onClick={() => setIsRunning(true)}>Start</button>
<p>{currentSecond}</p>
</div>
</div>
);
}
I went ahead and put an example codepen together for your use case if you want to play around with it and see how it works.
https://codepen.io/BastionTheDev/pen/XWbvboX

That is because you're code is closing over the currentSecond value from the render before you clicked on the button. That is javascript does not know about re-renders and hooks. You do want to set this up slightly differently.
import React, { useState, useRef, useEffect } from 'react';
const Play = ({ secondsPerRep }) => {
const secondsPassed = useRef(1)
const [currentSecond, setCurrentSecond] = useState(1);
const [timerStarted, setTimerStarted] = useState(false)
useEffect(() => {
let timer;
if(timerStarted) {
timer = setInterval(() => {
if (secondsPassed.current < secondsPerRep) {
secondsPassed.current =+ 1
setCurrentSecond(secondsPassed.current)
}
}, 1000);
}
return () => void clearInterval(timer)
}, [timerStarted])
return (
<div>
<div>
<button onClick={() => setTimerStarted(!timerStarted)}>
{timerStarted ? Stop : Start}
</button>
<p>{currentSecond}</p>
</div>
</div>
);
}
export default Play;
Why do you need a ref and the state? If you would only have the state the cleanup method of the effect would run every time you update your state. Therefore, you don't want your state to influence your effect. You can achieve this by using the ref to count the seconds. Changes to the ref won't run the effect or clean it up.
However, you also need the state because you want your component to re-render once your condition is met. But since the updater methods for the state (i.e. setCurrentSecond) are constant they also don't influence the effect.
Last but not least I've decoupled setting up the interval from your counting logic. I've done this with an extra state that switches between true and false. So when you click your button the state switches to true, the effect is run and everything is set up. If you're components unmounts, or you stop the timer, or the secondsPerRep prop changes the old interval is cleared and a new one is set up.
Hope that helps!

Try that. The problem was that you're not using the state that is received by the setCurrentSecond function and the function setInterval don't see the state changing.
const Play = props => {
const [currentSecond, setCurrentSecond] = useState(1);
const [timer, setTimer] = useState();
const onClick = () => {
setTimer(setInterval(() => {
setCurrentSecond((state) => {
if (state < props.secondsPerRep) {
return state + 1;
}
return state;
});
}, 1000));
}
return (
<div>
<div>
<button onClick={onClick} disabled={timer}>Start</button>
<p>{currentSecond}</p>
</div>
</div>
);
}

Related

Call function when time changes in react

Being new to react , this all is really confusing and new to me , so I apologise if I'm making some obvious oversight.
Im making a stopwatch and implementing the seconds for starters. However; Im confused as to how i'll implement the on display seconds number to update when each second passes.
This is what I'm doing right now
function App() {
const [time , updateTime] = React.useState(0);
var startsec = 0;
//UpdateTime should get triggered when next second passes
const UpdateTime = () => {
//Update time variable with the new seconds elapsed
}
//Should run every second or something
const CheckTimeUpdation = () => {
currentsec = Math.floor(Date.now() / 1000.0);
console.log(currentsec);
if(currentsec > startsec){
UpdateTime(currentsec-startsec);
}
}
const GetStartTime = () => {
startsec = Math.floor(Date.now() / 1000.0);
}
//Clock component just gets a number and displays it on the screen
return (<div className = "App">
<Clock timerSeconds= {time}/>
<div>
<button onClick={GetStartTime}></button>
</div>
</div>);
}
export default App;
Date.now() function gets the miliseconds passed since 1970 (hence the division by 1000 to make them into seconds) and I find the difference between when the button was clicked and current one and passs that to the time component to display.
How do I make the CheckTimeUpdation function run every second or so?
What you want is the setInterval() method (see: https://developer.mozilla.org/en-US/docs/Web/API/setInterval)
However your code so far has some issues:
On the button click, getStartTime runs and it updates the value of startsec. Firstly, this does not cause the component to re-render, and so the component will not update and you will see nothing changing on your screen. Also, if you did get your component to re-render, you will notice that startsec will be 0 again on the next re-render, so re-assigning startsec like how you did likely doesn't do what you want it to. If you want to persist values between rerenders, you can use useState (https://reactjs.org/docs/hooks-reference.html#usestate) or useRef (https://reactjs.org/docs/hooks-reference.html#useref).
Now i'm assuming you want to start the timer when the button is clicked. What you need is to start the interval (via setInterval) on the button click, and update time every 1000ms.
You'd also want to clear the interval once you don't need it anymore, using the clearInterval() method. You'll need to save the id returned from setInterval() initially in order to clear it later.
Below I have written a short example using this idea with setInterval on button click to help you:
import { useState, useRef } from "react";
export default function App() {
const [timerState, setTimerState] = useState("paused");
const [timeElapsed, setTimeElapsed] = useState(0);
const intervalId = useRef(0);
const isRunning = timerState === "running";
const isPaused = timerState === "paused";
const onStartTimer = () => {
intervalId.current = setInterval(() => {
setTimeElapsed((time) => time + 1);
}, 1000);
setTimerState("running");
};
const onStopTimer = () => {
clearInterval(intervalId.current);
intervalId.current = 0;
setTimerState("paused");
};
const onClearTimer = () => {
setTimeElapsed(0);
};
return (
<div className="App">
<h1>{timeElapsed}</h1>
{isPaused && <button onClick={onStartTimer}>Start timer</button>}
{isRunning && <button onClick={onStopTimer}>Stop timer</button>}
{!!timeElapsed && <button onClick={onClearTimer}>Clear timer</button>}
</div>
);
}
You can use setInterval in the GetStartTime Function.
const GetStartTime = () => {
startsec = Math.floor(Date.now() / 1000.0);
setInterval(() => CheckTimeUpdation(), 1000);
}

How do I loop an image carousel with React setState and setInterval?

I am trying to set up an image carousel that loops through 3 images when you mouseover a div. I'm having trouble trying to figure out how to reset the loop after it reaches the third image. I need to reset the setInterval so it starts again and continuously loops through the images when you are hovering over the div. Then when you mouseout of the div, the loop needs to stop and reset to the initial state of 0. Here is the Code Sandbox:
https://codesandbox.io/s/pedantic-lake-wn3s7
import React, { useState, useEffect } from "react";
import { images } from "./Data";
import "./styles.css";
export default function App() {
let timer;
const [count, setCount] = useState(0);
const updateCount = () => {
timer = setInterval(() => {
setCount((prevCount) => prevCount + 1);
}, 1000);
if (count === 3) clearInterval(timer);
};
const origCount = () => {
clearInterval(timer);
setCount((count) => 0);
};
return (
<div className="App">
<div className="title">Image Rotate</div>
<div onMouseOver={updateCount} onMouseOut={origCount}>
<img src={images[count].source} alt={images.name} />
<p>count is: {count}</p>
</div>
</div>
);
}
Anything involving timers/intervals is an excellent candidate for useEffect, because we can easily register a clear action in the same place that we set the timer using effects with cleanup. This avoids the common pitfalls of forgetting to clear an interval, e.g. when the component unmounts, or losing track of interval handles. Try something like the following:
import React, { useState, useEffect } from "react";
import { images } from "./Data";
import "./styles.css";
export default function App() {
const [count, setCount] = useState(0);
const [mousedOver, setMousedOver] = useState(false);
useEffect(() => {
// set an interval timer if we are currently moused over
if (mousedOver) {
const timer = setInterval(() => {
// cycle prevCount using mod instead of checking for hard-coded length
setCount((prevCount) => (prevCount + 1) % images.length);
}, 1000);
// automatically clear timer the next time this effect is fired or
// the component is unmounted
return () => clearInterval(timer);
} else {
// otherwise (not moused over), reset the counter
setCount(0);
}
// the dependency on mousedOver means that this effect is fired
// every time mousedOver changes
}, [mousedOver]);
return (
<div className="App">
<div className="title">Image Rotate</div>
<div
// just set mousedOver here instead of calling update/origCount
onMouseOver={() => setMousedOver(true)}
onMouseOut={() => setMousedOver(false)}
>
<img src={images[count].source} alt={images.name} />
<p>count is: {count}</p>
</div>
</div>
);
}
As to why your code didn't work, a few things:
You meant to say if (count === 2) ..., not count === 3. Even better would be to use the length of the images array instead of hardcoding it
Moreover, the value of count was stale inside of the closure, i.e. after you updated it using setCount, the old value of count was still captured inside of updateCount. This is actually the reason to use functional state updates, which you did when you said e.g. setCount((prevCount) => prevCount + 1)
You would have needed to loop the count inside the interval, not clear the interval on mouse over. If you think through the logic of it carefully, this should hopefully be obvious
In general in react, using a function local variable like timer is not going to do what you expect. Always use state and effects, and in rarer cases (not this one), some of the other hooks like refs
I believe that setInterval does not work well with function components. Since callback accesses variables through closure, it's really easy to shoot own foot and either get timer callback referring to stale values or even have multiple intervals running concurrently. Not telling you cannot overcome that, but using setTimeout is much much much easier to use
useEffect(() => {
if(state === 3) return;
const timerId = setTimeout(() => setState(old => old + 1), 5000);
return () => clearTimeout(timerId);
}, [state]);
Maybe in this particular case cleanup(clearTimeout) is not required, but for example if user is able to switch images manually, we'd like to delay next auto-change.
The timer reference is reset each render cycle, store it in a React ref so it persists.
The initial count state is closed over in interval callback scope.
There are only 3 images so the last slide will be index 2, not 3. You should compare against the length of the array instead of hard coding it.
You can just compute the image index by taking the modulus of count state by the array length.
Code:
export default function App() {
const timerRef = useRef();
const [count, setCount] = useState(0);
// clear any running intervals when unmounting
useEffect(() => () => clearInterval(timerRef.current), []);
const updateCount = () => {
timerRef.current = setInterval(() => {
setCount((count) => count + 1);
}, 1000);
};
const origCount = () => {
clearInterval(timerRef.current);
setCount(0);
};
return (
<div className="App">
<div className="title">Image Rotate</div>
<div onMouseOver={updateCount} onMouseOut={origCount}>
<img
src={images[count % images.length].source} // <-- computed index to cycle
alt={images.name}
/>
<p>count is: {count}</p>
</div>
</div>
);
}
Your setCount should use a condition to check to see if it should go back to the start:
setCount((prevCount) => prevCount === images.length - 1 ? 0 : prevCount + 1);
This will do setCount(0) if we're on the last image—otherwise, it will do setCount(prevCount + 1).
A faster (and potentially more readable) way of doing this would be:
setCount((prevCount) => (prevCount + 1) % images.length);

State update faster than alert display

import React from 'react';
const Comp2 = () => {
const [count, setCount] = React.useState(0);
const handleIncrease = () => {
setCount((x) => x + 1);
};
const checkCurrentCount = () => {
console.log('checking...');
setTimeout(() => {
alert(`Ok the current count is: ${count}`);
}, 2000);
};
return (
<div>
<p>{count}</p>
<button onClick={handleIncrease}>+</button>
<button onClick={checkCurrentCount}>check count</button>
</div>
);
};
Problem
If the count number already change, but the alert shows the past number. How to encounter this problem? the setTimeout just simulation of the problem..
You can fix this issue with help of useRef hook.
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.
View the solution on code sandbox
https://codesandbox.io/s/priceless-newton-l66q6?file=/src/App.js
You need clearTimeout and setTimeout again
CodeSandBox

setInterval in react does not update the setState

I want to make a countdown timer (25 minutes). This is the code in App.js file
import './App.css';
import React, { useState } from 'react';
function App() {
const [remainingTime, setRemainingTime] = useState(1500);
const [seconds, setSeconds] = useState(0);
const [minute, setMinute] = useState(remainingTime/60);
function timer() {
setRemainingTime(remainingTime-1);
let newMinute = remainingTime/60;
let minuteArray = [...newMinute.toString()];
setMinute(parseInt(minuteArray.slice(0,2).join("")));
setSeconds(remainingTime%60);
console.log(minute);
console.log(seconds);
}
return (
<div className="App">
<div className="pomodoro">
<div className="timer">{minute}:{seconds}</div>
<div className="button-container">
<button className="start" onClick={() => setInterval(timer, 1000)}>start</button>
</div>
</div>
</div>
);
}
export default App;
The interval does not update the state value. The minute value always 25 and the seconds value always 0. When I don't use setInterval and just use the timer function like this
<button className="start" onClick={timer}>start</button>
every time I click the start button, the value changes. Any idea? I know I should use clearInterval too but I don't know where to put it. Should I create a new function which contain setInterval and clearInterval?
Issue
The main issue is stale enclosures of state in the timer callback.
Within the interval callback, the standard form of setting a new value doesn't work because the callback keeps using the same (non-updated) state value each time it runs:
—this doesn't work: setValue(value + 1)
Solution
Use the alternate form of setting new value:
—this will work: setValue((value) => value + 1)
This form allows the callback to obtain a fresh value with the most recent state every time it runs.
Details
The minute and seconds is considered derived state (from remainingTime) so it shouldn't also be stored in state, it is easily computed from state.
Use a functional state update to update the remainingTime state from the previous state, not the state of the render cycle the callback was enqueued in.
Use a React ref to hold an interval timer ref, so the interval can be cleared.
Use an useEffect hook to return a clean up function to clear any running intervals when the component unmounts.
Code:
function App() {
const timerRef = React.useRef();
React.useEffect(() => {
return () => clearInterval(timerRef.current);
}, []);
const [remainingTime, setRemainingTime] = React.useState(1500);
function timer() {
setRemainingTime((remainingTime) => remainingTime - 1);
}
const startTimer = () => {
clearInterval(timerRef.current); // clear any running interval
setRemainingTime(1500); // reset state back to 25 minutes
timerRef.current = setInterval(timer, 1000); // start/restart interval
};
const minute = String(Math.floor(remainingTime / 60)).padStart(2, 0);
const seconds = String(remainingTime % 60).padStart(2, 0);
return (
<div className="App">
<div className="pomodoro">
<div className="timer">
{minute}:{seconds}
</div>
<div className="button-container">
<button className="start" onClick={startTimer}>
start
</button>
</div>
</div>
</div>
);
}
Demo
Since this will likely be a follow-up question, use another useEffect to "listen" to the remainingTime state and when the time hits 0, clear the interval, reset remainingTime back 1500 (for display), and show any alarm or alert or whatever for the pomodoro break/schedule.
On every rerender, timer is set as 1500. Use closure here, activated on click.
function timer() {
let t = remainingTime;
setInterval(() => {
t -= 1;
setRemainingTime(t);
let newMinute = t / 60;
let minuteArray = [...newMinute.toString()];
setMinute(parseInt(minuteArray.slice(0, 2).join("")));
setSeconds(t % 60);
console.log(minute);
console.log(seconds);
}, 1000); }

updating current time every second in react without rendering

Many of my components in a react native app require to know what the current time is every second. This way I can show updated real-time information.
I created a simple functionality to set the state with new Date(), but whenever I set the state, the component re-renders, which is a waste my case.
Here is what I have:
...
export default function App() {
const [currentDateTime, setCurrentDateTime] = useState(() => new Date().toLocaleString());
useEffect(() => {
const secondsTimer = setInterval(() => {
setCurrentDateTime(new Date().toLocaleString());
}, 1000);
return () => clearInterval(secondsTimer);
}, [setCurrentDateTime]);
console.log('RENDERING');
<Text>{currentDateTime}</Text>
...
I can see the console logs RENDERING every second.
Is there a way to avoid this rerendering and still update currentDateTime
Consider using shouldComponentUpdate lifecycle method; It's purpose is for preventing unnecessary renders. Add this method and tell your component not to update if this particular part of your state changes. As an example, you might add this shouldComponentUpdate() that rejects updates that are more than
// Example logic for only re-rendering every 5 seconds; Adapt as needed.
shouldComponentUpdate(nextProps, nextState) {
if (this.lastUpdatedTimeInSeconds+5 >= nextState.timeinseconds) {
return false;
}
this.lastUpdatedTimeInSeconds = nextState.timeinseconds
return true;
}
Further Reading: https://developmentarc.gitbooks.io/react-indepth/content/life_cycle/update/using_should_component_update.html
If I understand what you're saying, you want to update the DOM without triggering React's lifecycle. This is possible using refs (see React.useRef):
import * as React from "react";
import "./styles.css";
export default function App() {
const dateTimeRef = React.useRef<HTMLSpanElement>(null);
console.log("RENDERING");
React.useEffect(() => {
const secondsTimer = setInterval(() => {
if (dateTimeRef.current) {
dateTimeRef.current.innerText = new Date().toLocaleString()
}
}, 1000);
return () => clearInterval(secondsTimer);
}, []);
return <span ref={dateTimeRef} />;
}
See working demo - https://codesandbox.io/s/nice-snow-kt500?file=/src/App.tsx
Update 1
If you want to use a component such as Text, then the component will have to forward the ref to the dom, like here:
import * as React from "react";
import "./styles.css";
const Text = React.forwardRef<HTMLSpanElement>((props: any, ref) => {
console.log("RENDERING TEXT")
return <span ref={ref}></span>
});
export default function App() {
const dateTimeRef = React.useRef<HTMLSpanElement>(null);
console.log("RENDERING APP");
React.useEffect(() => {
const secondsTimer = setInterval(() => {
if (dateTimeRef.current) {
dateTimeRef.current.innerText = new Date().toLocaleString();
}
}, 1000);
return () => clearInterval(secondsTimer);
}, []);
return <Text ref={dateTimeRef} />;
}
See working demo - https://codesandbox.io/s/jolly-moon-9zsh2?file=/src/App.tsx
Eliya Cohen's answer was conceptually correct. To avoid re-rendering, we cannot use state with an interval. We need to reference the element. Unfortunately, I wasn't able to adopt Eliya's React code to React Native in the same manner, so I did some more digging and found docs on directly manipulating React Native components.
In short, you can manipulate built in RN components' PROPS and avoid re-rendering by not changing the state.
Since the <Text> component doesn't set its value with a prop, such as <Text text="my text" />, we are not able to use this method to update it. But what does work is updating the value of a TextInput since its set with the value prop. All we need to do to make the <TextInput> behave like a <Text> is to set its prop editable to false, and of course avoid default styling of it that would make it look like an input.
Here is my solution. If someone has a better one, please do propose it.
import React, { useEffect } from 'react';
import { TextInput } from 'react-native';
const Timer: React.FC = () => {
updateTime = (currentTime) => {
time.setNativeProps({ text: currentTime });
};
useEffect(() => {
const secondsTimer = setInterval(() => {
updateTime(new Date().toLocaleString());
}, 1000);
return () => clearInterval(secondsTimer);
}, []);
return <TextInput ref={(component) => (time = component)} editable={false} />;
};
export default Timer;
I also tried this and this is what that worked for me after a few attempts with Typescript.
const timeTextInput = useRef<TextInput>(null);
useEffect(()=>{
const timer = setInterval(() => {
timeTextInput.current?.setNativeProps({ text: new Date().toLocaleString() });
}, 1000);
return () => clearInterval(timer);
}, []);
Hope this helps someone in the future.

Resources