React's useEffect unstable delay - javascript

I'm working on a Pomodoro clock. To build the countdowns I'm using useEffect and setTimeout. Everything seemed to be fine until I realized there's a 30ms to 50ms delay between every second of the clock. How would I set it to pricesily update the clock at every 1000ms?
I'm using useState to handle the clock's time and controls to stop, pause and reset. They're all working properly. It's just the second's timing that is delaying more that it should.
function App() {
const [workTarget, setWorkTarget] = useState(25 * 60);
const [breakTarget, setBreakTarget] = useState(5 * 60);
const [time, setTime] = useState(workTarget); //time in seconds
const [counting, setCounting] = useState(false);
const [working, setWorking] = useState(true);
const [clockTarget, setClockTarget] = useState(workTarget);
const [combo, setCombo] = useState(0);
const [config, setConfig] = useState(false);
const [playWork] = useSound(workSfx);
const [playBreak] = useSound(breakSfx);
const [playPause] = useSound(pauseSfx);
let tick = 1000;
let timeout;
let timenow = Date.now();
// Handle pause and stop of countdown
useEffect(() => {
if (time > 0 && counting === true) {
timeout = setTimeout(() => {
setTime(time - 1);
console.log(timenow);
}, tick);
} else if (time === 0 && counting === true) {
setWorking(!working);
if (working === true) {
playBreak();
setTime(breakTarget);
setClockTarget(breakTarget);
} else {
playWork();
setCombo(combo + 1);
setTime(workTarget);
setClockTarget(workTarget);
}
}
if (!counting || config) {
clearTimeout(timeout);
}
});
}
export default App;
This is not the complete code. I cut off other components for buttons and stuff that don't relate to this.

We can't ensure its EXACTLY 1000 ms. Behind the scenes, react state updates use setTimeout, and to change the timer, you need to use either setTimeout or setInterval. setTimeout and setInterval only ensure that the code inside will not run for a delay period, and then executes when the main thread is not busy.
Therefore, it's impossible to ensure that every update is EXACTLY 1000ms. There will usually be 30-50ms delay.
However, that doesn't mean your timer will be inaccurate or unreliable. It just depends how you initialize it.
Below is how I would improve the code you provided, as right now clearing the timeout adds some extra overhead, and batching the state updates would lead to improved performance in this case.
let tick = 1000;
//countdown modifying behavior every 1000ms. Active clock
useEffect(() => {
if (time > 0 && counting === true) {
setTimeout(() => {
setTime((state) => state - 1);
console.log(timenow);
}, tick);
}
}, [time, counting]);
//pausing, stopping and resuming
useEffect(() => {
if (time === 0 && counting === true) {
setWorking(!working);
if (working === true) {
playBreak();
unstable_batchedUpdates(() =>{
setTime(breakTarget);
setClockTarget(breakTarget);
}
} else {
playWork();
//update all at once
unstable_batchedUpdates(() =>{
setCombo(combo + 1);
setTime(workTarget);
setClockTarget(workTarget);
}
}
}
}, [time, counting]);
Further improvements to keep the clock in sync
On component mount, grab the current time for start time, and calculate the end time and store both
At every setTimeout in your useEffect, using your stored start time and current time, calculate how much time there is left and set state to that.
//countdown time modifying behavior every 1000ms
const current = new Date()
const [start, setStart] = useState(current)
//this example uses a 25 minute timer as an example
const [end, setEnd] = useState(new Date(start.getTime() + 25 * 60000))
const tick = 1000
useEffect(() => {
if (time > 0 && counting === true) {
setTimeout(() => {
const current = new Date()
const diffTime = current.getDate() - end.getDate()
const timeRemainingInMins = Math.ceil(diffTime / (1000*60));
setTime(timeRemainingInMins);
}, tick);
}
}, [time, counting]);

I may have an idea.
Currently your useEffect hook is set up to run on every render which means every time you change the Time you render the page and rerun the useEffect hook, however you seem to have quite a bit of logic inside the hook, plus react may not render consistently which means that there may be some inconsistencies with your timing.
I suggest using setInterval instead and running the useEffect hook only once at the start to set up the interval using useEffect(() => {...}, []) (notice [] as the second argument)
For the rest of your logic you can always create another useEffect hook that updates when the components change.
With all that in mind your timing logic should look something like this:
useEffect(() => {
interval = setInterval(() => {
setTime(time-1);
console.log(timenow);
}, tick); //runs once every tick
return(() => clearInterval(interval)); //Once the component unmounts run some code to clear the interval
}, [])
Now in another useEffect hook you can watch for changes to time and update the rest of your app accordingly.
This approach should be much more reliable because it doesn't depend on react to update the time.

#The_solution
const component =() => {
const [val, setval]= useState(globalState.get());
useEffect(() => {
setVal(globalState.get());
globalState.subscribe(newVal=> setVal(newVal));
});
return <span>{val}</span>
}

Related

How do I update a state each second for next n seconds

I have a state variable resendTime and magicLinkState.
I would like to setResendTime to resendTime - 1 for next 10 seconds when magicLinkState becomes sent.
I have tried the following code but this does not update resendTime every second.
useEffect(() => {
if (magicLinkState === "sent" && resendTime > 0) {
const timer = setInterval(() => setResendTime(resendTime - 1), 1000);
console.log(resendTime);
return () => clearInterval(timer);
}
}, []);
This code only updates the state for the first second. I have also tried passing dependency array but didn't worked.
How do I make it update the state so I achieve what is intended.
The general idea is correct in your code but needs some adjustments.
it's required to add the magicLinkState to the effect's dependencies otherwise it won't trigger when this gets the sent value but only if it's initially set with this value.
you should use the arrow syntax: setResendTime((prevResendTime) => prevResendTime - 1) in order to grab the correct state value each time
there is no need for a cleanup function in this effect since you want to clear the interval only after it's triggered and made 10 decreases to resendTime.
you should add some local count variable in order for the decrease to happen 10 times only and not forever
After these changes your code should look like below:
const decreaseResentTime = (prevResendTime) => prevResendTime - 1;
useEffect(() => {
let timer;
if (magicLinkState === "sent" && resendTime > 0) {
let count = 0;
timer = setInterval(() => {
if (count < 10) {
count++;
setResendTime(decreaseResentTime );
} else {
clearInterval(timer);
}
}, 1000);
}
}, [magicLinkState]);
You can find an example that demonstrates this solution in this CodeSandbox.
There are more improvements to be made here but they are based on your needs. For example if the magicLinkState changes to 'sent' and then to something else and then back to 'sent' within the 10 seconds, 2 intervals will run and decrease at a double rate.
You've just an issue of a stale closure over the initial resendTime state value. This is easily fixed by using a functional state update to correctly access the previous state value instead of whatever is closed over in the surround callback scope.
const timerRef = React.useRef();
useEffect(() => {
if (magicLinkState === "sent" && resendTime > 0) {
const timer = setInterval(() => {
setResendTime(resendTime => resendTime - 1); // <-- functional update
}, 1000);
timerRef.current = timer;
return () => clearInterval(timer);
}
}, []);
Note also that because of the closure that if you want to log the resendTime state as it updates you'll need to use a another useEffect hook with a proper dependency. This is also where you'd move the logic to check if the interval is completed. Use a React ref to store a reference to the timer id.
useEffect(() => {
if (resendTime <= 0) {
clearInterval(timerRef.current);
}
console.log(resendTime);
}, [resendTime]);
Can I use timestamps instead of intervals? this function checks every second for 10 seconds when called once
useEffect(() => {
if (magicLinkState === "sent" && resendTime > 0) {
let startTimeforTenSeconds = Date.now();
let startTimeforEachSecond = Date.now();
const nonBlockingCommand = new Promise((resolve, reject) => {
while (true) {
const currentTime = Date.now();
if (currentTime - startTimeforEachSecond >= 1000) {
startTimeforEachSecond = Date.now();
console.log("1 seconds have passed. set resend time here");
// code for resending time here
setResendTime(resendTime - 1);
console.log(resendTime );
}
if (currentTime - startTimeforTenSeconds >= 10000) {
console.log("10 seconds have passed. stop checking.");
resolve("10 seconds have passed.");
break;
}
}
});
nonBlockingCommand.then(() => console.log('function is done'));
}
}, []);
EDIT: Result screenshot:

why InfiniteScroll implementation updated only one time in React table?

this is the fetchData function and it works the first time, the table updated 10 rows, but when you reach the end again nothing happens . what is the reason for this to work the first time only and not updating after ?
const [items , setItems] = useState(data);
const [hasMore , setHasMore] = useState(true)
const [offset, setOffset] = useState(10);
const fetchMoreData = () => {
if (items.length >= items.length +1) {
setHasMore( false );
return;
}
setTimeout(() => {
setOffset( offset + 10);
}, 100);
};
You would need to provide a bit more code than this, but I suppose what you'll want to do is listen for changes in useEffect, and based on a condition, call your fetch function

Play video smoothly with setInterval in React

I want to play a video with the setInterval function which should set the currentTime every Frame to play the video at the usual speed. My video has 25 frames per second. And to make it smoother I thought of using requestAnimationFrame, as without it it's lagging a bit. But I'm lost in the React useEffect Architecture on how to unmount all different life cycles in the right way. So how to simultaneously use setInterval with requestAnimationFrame to set the currentTime correctly.
The comments show
import React, { useState, useEffect, useRef } from "react";
export default function VideoInterval() {
const videoRef = useRef(null);
const [currentFrame, setCurrentFrame] = useState(0);
useEffect(() => {
// Set Video Framerate
const framerate = 25;
// Calculate interval duration for fluid playback
const intervalDuration = 1000 / framerate;
const interval = setInterval(() => {
// Not sure if it's a bad idea to store it in a let variable or this will cause problems
// let currentTime = videoRef.current.currentTime;
// Use requestAnimationFrame for smooth playback
const intervalPlay = () => {
videoRef.current.currentTime =
videoRef.current.currentTime + intervalDuration / 1000; // add interval in seconds
// window.requestAnimationFrame(intervalPlay);
};
window.requestAnimationFrame(intervalPlay);
// Tried it over useState to see if working with refs made it slower that useState
// setCurrentFrame((i) => i + intervalDuration / 1000);
// Not sure where or if to cancel the AnimationFrame
// cancelAnimationFrame(videoRef.current.currentTime);
}, intervalDuration); // every 40 milliseconds == 25 frames per second
return () => {
clearInterval(interval);
};
}, []);
return (
<>
<video
ref={videoRef}
// autoPlay
muted
loop
>
<source src="video_with_25_frames_per_second.mp4" type="video/mp4" />
</video>
</>
);
}

Is it possible to keep setInterval on multiple devices 'synced' by compensating for drift, or do I need server synchronisation?

I am building a React Native app where my entire back end is provided for by services like Firebase etc.
The app requires clocks on multiple devices to start and end at the same time which can run for up to an hour.
Given a shared starting point in time between devices I have observed drift in the accuracy of setInterval in this 20 seconds of data:
I am attempting to compensate for this deviation in clock timing by measuring it and then compensating for it - here is a code sandbox with my solution.
useTimer hook:
import { useState, useEffect, useRef } from "react";
import moment from "moment";
export const convertMsToMinsAndSecs = (countDown) => {
const seconds = moment
.duration(countDown)
.seconds()
.toString()
.padStart(2, "0");
const minutes = moment
.duration(countDown)
.minutes()
.toString()
.padStart(2, "0");
const minsAndSecs = `${minutes.toString()}:${seconds.toString()}`;
return countDown > 0 ? minsAndSecs : "00:00";
};
const roundTimeStamp = (timeStamp) =>
timeStamp === 0 ? 0 : timeStamp + (1000 - (timeStamp % 1000));
export const useTimer = (
started,
startTime,
length,
resetClock,
clockIntialState
) => {
const initialTimerState = {
start: 0,
end: 0,
timeNow: 0,
remaining: length,
clock: convertMsToMinsAndSecs(length),
internalClockDeviation: 0
};
const [timeData, setTimeData] = useState(initialTimerState);
const intervalId = useRef(null);
const deviation = useRef(null);
useEffect(() => {
setTimeData((prevState) => ({
...prevState,
start: roundTimeStamp(startTime),
end: roundTimeStamp(startTime) + length
}));
if (started) {
intervalId.current = setInterval(() => {
const intervalTime = moment().valueOf();
setTimeData((prevState) => {
return {
...prevState,
timeNow: intervalTime,
remaining: prevState.remaining - 1000,
clock: convertMsToMinsAndSecs(prevState.remaining - 1000),
internalClockDeviation:
prevState.timeNow === 0
? 0
: intervalTime - prevState.timeNow - 1000
};
});
}, 1000 - deviation.current);
}
}, [started]);
useEffect(() => {
deviation.current = timeData.internalClockDeviation;
}, [timeData.internalClockDeviation]);
if (timeData.remaining <= 0 && started) {
resetClock(clockIntialState);
clearTimeout(intervalId.current);
setTimeData(initialTimerState);
}
const compensatedLength = 1000 - deviation.current;
return {
timeData,
intervalId,
compensatedLength,
setTimeData,
initialTimerState
};
};
As I am not running my own server application I would prefer to handle this on the client side if possible. It also means that I do not need to rely on network connections or the availability of a timing server.
Will my approach work across multiple devices, and if so can it be improved, or do I need to build a server side application to effectively handle this? TIA.
When you determine time diff you can not rely on intervals being accurate. Gets worse when tab is in background/not in focus.
Typically you rely on timestamps to get the offset in time, you do not subtract a fix number.
function countDown(totalTime, onComplete, onUpdate, delay = 1000) {
let timer;
const startTime = new Date().getTime();
function next() {
const runningTime = new Date().getTime() - startTime;
let remaining = Math.max(totalTime - runningTime, 0);
onUpdate && onUpdate(remaining);
!remaining && onComplete && onComplete();
var ms = Math.min(delay, remaining);
timer = remaining && window.setTimeout(next, ms);
}
next()
return function () {
timer && window.clearTimeout(timer);
}
}
countDown(5000, function(){ console.log('done1'); }, function(x){ console.log('update1 ', x); });
const out = document.getElementById("out");
const cd = countDown(
60000,
function(){ out.textContent = 'done'; },
function(x){ out.textContent = (x/1000).toFixed(3); },
20
);
document.getElementById("btn").addEventListener('click', cd);
<div id="out"></div>
<button id="btn">stop</button>
This will fail if user changes clock, not much you can do on that. You could ping the server for time, but that also has latency with how long the call takes.
The only way you can synchronize all the clocks together is having all of them observe one single source of truth.
If you create the timestamp on one client. You need to make sure all the other clients are in sync with that data.
What you can do is have a server + client architecture where the server is the single point of truth. But if you are trying to completely synchronize without a single point of truth, you are doomed to fail because of the problems that you can not control like latency of communication between all the client applications in your case clock.

ReactJS useState not updating momentJS prop

I am coding a CRM app and there's a asnyc function for gettingTrackers and it working well. There's another function called calculateStartTime and this function suppose to calculate momentJS variable and set it but this is not updating.
useEffect(() => {
async function gettingTrackers() {
await getTrackers(null, null);
}
gettingTrackers();
calculateStartTime();
}, []);
const [startTime, setStartTime] = useState(moment().format("YYYY-MM-DD"));
const [endTime, setEndTime] = useState(moment().format("YYYY-MM-DD"));
const calculateStartTime = () => {
const dateOfMay = moment("2020-05-01");
const now = moment();
let timeTheStart = null;
let timeTheEnd = null;
if (now.add(-32, "d").diff(dateOfMay) <= 0) {
timeTheStart = dateOfMay.format("YYYY-MM-DD")
timeTheEnd = moment().add(-2, "d").format("YYYY-MM-DD");
} else {
timeTheStart = moment().add(-32, "d").format("YYYY-MM-DD");
timeTheEnd = moment().add(-2, "d").format("YYYY-MM-DD");
}
console.log("calculating...")
console.log("start time > ", timeTheStart)
console.log("end time > ", timeTheEnd);
setStartTime(moment(timeTheStart).format("YYYY-MM-DD"))
setEndTime(moment(timeTheEnd).format("YYYY-MM-DD"))
// these 2 logs prints initial value, not updated value.
console.log(startTime);
console.log(endTime)
}
The problem is that I have to send startTime and endTime to another ReactJS component, and it sends first initial today value every time. When I call calculateStartTime it logs
calculating...
start time > 2020-06-07
end time > 2020-07-07
But when I click to button for another component, I print these variables and its output;
2020-07-09
2020-07-09
as initial values. I log them with using startTime and endTime as I described in useState
What I am missing on this problem? Is there any memory-leak to not-working?
Edit:
const goToResultButton = (event, data) => {
event.preventDefault();
console.log("start time > ", startTime)
console.log("end time > ", endTime)
}
Thanks in advance!
With the below code block, you are setting the state and immediately you are trying to access the updated value, but state updates done in async fashion. You will get the latest value in the next re-render.
...
setStartTime(moment(timeTheStart).format("YYYY-MM-DD"))
setEndTime(moment(timeTheEnd).format("YYYY-MM-DD"))
// these 2 logs prints initial value, not updated value.
console.log(startTime);
console.log(endTime)
...
You can use a useEffect to log or do something with latest values of startTime and endTime.
useEffect(() => {
console.log("startTime", startTime);
console.log("endTime", endTime);
}, [startTime, endTime]);

Categories