useAnimationOnDidUpdate react hook implementation - javascript

So I want to use requestAnimationFrame to animate something using react hooks.
I want something small so react-spring/animated/react-motion is a bad choice for me.
I implemented useAnimationOnDidUpdate but it is working incorrectly, here is reproduction with details.
What's wrong here: on second click multiplier for animation starts with 1, but should always start with 0 (simple interpolation from 0 to 1).
So I'm trying to understand why the hook saved previous value though I started a new animation loop already.
Here is a full code listing for hook:
import { useState, useEffect, useRef } from 'react';
export function useAnimationOnDidUpdate(
easingName = 'linear',
duration = 500,
delay = 0,
deps = []
) {
const elapsed = useAnimationTimerOnDidUpdate(duration, delay, deps);
const n = Math.min(1, elapsed / duration);
return easing[easingName](n);
}
// https://github.com/streamich/ts-easing/blob/master/src/index.ts
const easing = {
linear: n => n,
elastic: n =>
n * (33 * n * n * n * n - 106 * n * n * n + 126 * n * n - 67 * n + 15),
inExpo: n => Math.pow(2, 10 * (n - 1)),
inOutCubic: (t) => t <.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1,
};
export function useAnimationTimerOnDidUpdate(duration = 1000, delay = 0, deps = []) {
const [elapsed, setTime] = useState(0);
const mountedRef = useRef(false);
useEffect(
() => {
let animationFrame, timerStop, start, timerDelay;
function onFrame() {
const newElapsed = Date.now() - start;
setTime(newElapsed);
if (newElapsed >= duration) {
console.log('>>>> end with time', newElapsed)
return;
}
loop();
}
function loop() {
animationFrame = requestAnimationFrame(onFrame);
}
function onStart() {
console.log('>>>> start with time', elapsed)
start = Date.now();
loop();
}
if (mountedRef.current) {
timerDelay = delay > 0 ? setTimeout(onStart, delay) : onStart();
} else {
mountedRef.current = true;
}
return () => {
clearTimeout(timerStop);
clearTimeout(timerDelay);
cancelAnimationFrame(animationFrame);
};
},
[duration, delay, ...deps]
);
return elapsed;
}

This problem with this hook is that it doesn't clean up the elapsedTime upon completion.
You can resolve this by adding setTime(0) to you onFrame function when the animation is expected to stop.
Like this:
function onFrame() {
const newElapsed = Date.now() - start;
if (newElapsed >= duration) {
console.log('>>>> end with time', newElapsed)
setTime(0)
return;
}
setTime(newElapsed);
loop();
}
I know it may seem weird that it doesn't reset itself. But bear in mind that your animation is making use of the same hook instance for both easing in and out. Therefore that cleanup is necessary.
Note: I've also move the setTime(newElapsed) line so that it's after the if statement since this isn't necessary if the if statement is true.
UPDATE:
To further improve how this works, you could move the setTime(0) to the return cleanup.
This would mean that you're onFrame function changes to:
function onFrame() {
const newElapsed = Date.now() - start;
if (newElapsed >= duration) {
console.log('>>>> end with time', newElapsed)
setTime(0)
return;
}
setTime(newElapsed);
loop();
}
And then update your return cleanup for useAnimationTimerOnDidUpdate to:
return () => {
clearTimeout(timerStop);
clearTimeout(timerDelay);
cancelAnimationFrame(animationFrame);
setTime(0);
};
I'm assuming that the reason your animation "isn't working properly" was because the component would flash. As far as my testing goes, this update fixes that.

Related

How to do a task in a time interval in javascript

inside JavaScript, how can I tell something to be done in a certain period of time? For example, for 5 seconds, the value of a variable will increase and it will stop after 5 seconds.
Of course, there are setinterval and settimeout, but they only do that work after that period of time is over. I want that work to be done during that time.
setInterval
You can use setInteveral, but it's mostly trash for everything I've ever attempted to use it for. In the code below, the counter will only increment to around 35 in 1 second; about 28 milliseconds per interval -
// initialize a counter
let counter = 0
// increment the counter on interval
let myprocess = setInterval(_ => counter++, 0)
// after 5 seconds, stop the process and log the counter value
setTimeout(_ => { clearInterval(myprocess); console.log(counter) }, 1000)
A more appropriate utility would be setImmediate. It is supported in Node but not in browsers yet. Polyfills are available if you plan to use this in the browser.
async generators
Above we see setInterval is quite slow. Another approach is to use an asynchronous generator. In the code below, the counter can reach over 300,000 in just 1 second; About 3.3 microseconds per interval.
async function *count(until) {
let counter = 0
while (Date.now() < until) {
yield counter++
}
}
async function main() {
for await (const n of count(Date.now() + 1000))
document.write(`${n}, `)
return "done"
}
main().then(console.log)
If the goal is to run the process as fast as possible for the fixed duration, we will skip expensive operations like document.write. In the example below the counter can reach over 1M in just 1 second; just under 1 microsecond per interval -
async function *count(until) {
let counter = 0
while (Date.now() < until) {
yield counter++
}
}
async function run(process, duration) {
let result
for await (const n of process(Date.now() + duration))
result = n
return result
}
run(count, 1000).then(console.log)
// 1045592
blocking while loop
If the loop itself is the main part of your program and you want simply want to run the counter as fast as possible for a fixed duration, use a while loop. Note this will block all other code from running for the entire duration of the loop -
function count(until) {
let n = 0
while (Date.now() < until)
n++
return n
}
setTimeout(console.log, 0, "I was blocked")
console.log(count(Date.now() + 1000))
// 36618673
// "I was blocked"
You will need to check inside some sort of loop if the time has passed, make sure the loop is async in nature or the JS main thread will have issues.
I have modified PI calculator from here Javascript: PI (π) Calculator to run for 5 seconds instead of infinite
, it's using requestAnimationFrame to keep things async->
function * generateDigitsOfPi() {
let q = 1n;
let r = 180n;
let t = 60n;
let i = 2n;
while (true) {
let digit = ((i * 27n - 12n) * q + r * 5n) / (t * 5n);
yield Number(digit);
let u = i * 3n;
u = (u + 1n) * 3n * (u + 2n);
r = u * 10n * (q * (i * 5n - 2n) + r - t * digit);
q *= 10n * i * (i++ * 2n - 1n);
t *= u;
}
}
// Demo
let iter = generateDigitsOfPi();
const tmStart = Date.now();//
let output = document.querySelector("div");
(function displayNextDigit() {
output.insertAdjacentHTML("beforeend", iter.next().value);
scrollTo(0, document.body.scrollHeight);
if (Date.now() - tmStart < 5000) requestAnimationFrame(displayNextDigit);
})();
div { word-wrap:break-word; font-family: monospace }
<div></div>
const setRunInSeconds = (callback, ms = 1000, delay = 1) => {
const intervalId = setInterval(callback, delay)
setInterval(() => {
clearInterval(intervalId)
}, ms)
}
let num = 0
setRunInSeconds(() => {
console.log('interval: ' + num)
num += 1
}, 5000, 1)

Refreshing and updating useState/useRef in React

Trying to update and read state in React to switch between two different timers. I'm still new to programming and can't figure out why my component that displays the state "Session" or "Break" updates, but my conditional switchTimers() fails to switch timers based on that state.
const [timers, setTimers] = useState({
sessionTime: 25,
breakTime: 5,
});
const [timerDisplay, setTimerDisplay] = useState("Session");
const [timerActive, setTimerActive] = useState(false)
const [displayCount, setDisplayCount] = useState(1500);
const round = useRef();
function startStop(action, secondsLeft) {
const interval = 1000;
let expected = Date.now() + interval;
if (action === "start") {
setTimerActive(true)
round.current = setTimeout(step, interval);
function step() {
if (secondsLeft > 0) {
const drift = Date.now() - expected;
setDisplayCount((prevValue) => prevValue - 1);
secondsLeft --
expected += interval;
round.current = setTimeout(step, Math.max(0, interval - drift));
} else {
clearTimeout(round.current)
switchTimers()
}
}
} else if (action === "stop") {
clearTimeout(round.current);
setTimerActive(false);
}
}
function switchTimers() {
beep.current.play()
if (timerDisplay === "Session") {
setTimerDisplay("Break");
setDisplayCount(timers.breakTime * 60);
startStop("start", timers.breakTime * 60)
} else if (timerDisplay === "Break") {
setTimerDisplay("Session");
setDisplayCount(timers.sessionTime * 60);
startStop("start", timers.sessionTime * 60)
}
}
When the "Session" timer ends, it shows "Break" in my label that prints timerDisplay, but once "Break" timer ends, it reruns "Break" instead of switching to "Session". Any insight into whats going wrong?
Try writing you entire switchTimers function as a callback to the useEffect hook and add timerDisplay as a dependency. Also, please show the JSX you return from the component i.e. the entire code so that I can help you better.

IPhone / IPad : Vanilla JS scrollTop isn't working on Chrome

Vue CLI 3 , Vue 2.5.17
I want to scroll to Specific div element when RestApi response is success. But my code isn't working to IOS devices.
I try to "scroll", "scrollTo" and "scrollTop" functions. But those functions aren't working.
const el = document.querySelector(selector)
// Custom Scroll Class Method
Scroll.scrollIt(el, 200, 'linear')
// Common ScrollTo Method
document.querySelector('html', 'body')
.scrollTo({
top: (el.offsetTop - 90),
left: 0,
behavior: 'smooth'
})
ScrollIt function is a custom Scroll class method.
export default class Scroll {
public static scrollIt (element: HTMLElement, duration: number, callback?: Function) {
const start = document.querySelector('document, html, body').scrollTop
const change = element.offsetTop - start
let currentTime = 0
const increment = 20
const animateScroll = function () {
currentTime += increment
const val = easeInOutQuad(currentTime, start, change, duration)
document.querySelector('document, html, body').scrollTop = val - 90
if (currentTime < duration) {
setTimeout(animateScroll, increment)
}
}
animateScroll()
// t = current time
// b = start value
// c = change in value
// d = duration
function easeInOutQuad (t: number, b: number, c: number, d: number) {
t /= d/2
if (t < 1) return c/2*t*t + b
t--
return -c/2 * (t*(t-2) - 1) + b
}
}
}

How to return a Promise with setInterval()

I am trying to return a Promise object ever 1000ms, but i am not sure how to access the data returned in the Promise when it is inside a setInterval() callback.
EDIT
I appears i was not being very clear as to my intentions, so i will try and explain what it is i am trying to do. I making a count down where by the necessary calculations are are don every 1000ms based on specified end date.
Here is the code that provides the return value i would like returned as a Pormise value every 1000ms:
calculateTimeRemaining(endDate: string) {
const { secondsInDay, daysOfYear, secondsInHour, secondsInMinute } = this.unitsOfTime;
let distance: number =
(Date.parse(new Date(endDate).toString()) - Date.parse(new Date().toString())) / this.increment;
if (distance > 0) {
// Years left
if (distance >= daysOfYear * secondsInDay) {
// 365.25 * 24 * 60 * 60
this.timeRemaining.years = Math.floor(distance / (daysOfYear * secondsInDay));
distance -= this.timeRemaining.years * daysOfYear * secondsInDay;
}
// Days left
if (distance >= secondsInDay) {
// 24 * 60 * 60
this.timeRemaining.days = Math.floor(distance / secondsInDay);
distance -= this.timeRemaining.days * secondsInDay;
}
// Hours left
if (distance >= secondsInHour) {
// 60 * 60
this.timeRemaining.hours = Math.floor(distance / secondsInHour);
distance -= this.timeRemaining.hours * secondsInHour;
}
// Minutes left
if (distance >= secondsInMinute) {
// 60
this.timeRemaining.minutes = Math.floor(distance / secondsInMinute);
distance -= this.timeRemaining.minutes * secondsInMinute;
}
// Seconds left
this.timeRemaining.seconds = distance;
}
return this.timeRemaining;
}
Example:
const interval = window.setInterval(() => {
return new Promise((resolve, reject) => {
resolve('Hello');
});
}, 1000);
How to i access the Promise object with .then() afterwards?
Does not work:
interval.then((data) => console.log(data);
What you are looking for is an Observable, not a Promise. With promises, the callback you pass to then is executed at most once, so this:
interval.then((data) => console.log(data));
...will never print "Hello" more than once, even if you corrected the following mistakes in your code:
Whatever you return in a setInterval callback function is ignored.
setInterval does not return a promise, but an integer number, uniquely identifying the interval timer that was created.
On the other hand, an Observable can emit multiple events, contrary to a Promise.
There is an Observable proposal for EcmaScript, but you could create your own -- very simplified -- version of it:
class Observable {
constructor(exec) {
this.listeners = new Set;
exec({
next: (value) => this.listeners.forEach(({next}) => next && next(value)),
error: (err) => this.listeners.forEach(({error}) => error && error(err)),
complete: () => this.listeners.forEach(({complete}) => complete && complete())
});
}
subscribe(listeners) {
this.listeners.add(listeners);
return { unsubscribe: () => this.listeners.delete(listeners) }
}
}
// Create an Observable instead of a Promise;
const interval = new Observable(({next}) => {
setInterval(() => next("Hello"), 1000);
});
// Subscribe to that Observable
const subscription = interval.subscribe({ next: (data) => console.log(data) });
// Optionally use the returned subscription object to stop listening:
document.querySelector("button").addEventListener("click", subscription.unsubscribe);
<button>Stop listening</button>
Note that several JavaScript frameworks have an implementation of Observable.
Depending on what you're actually trying to do, an async iterable might do the job.
The difference is that an async iterable will only generate the next promise if you consume the last one. Intervals in JavaScript are tricky, even without promises. They try to run their callback at regular intervals, but the execution of any callback may be delayed if the interpreter is busy. That delay will not propagate, though. Also, short intervals will be throttled for background tabs.
Assuming your code is always waiting to consume the async iterable (e.g. in a for…of loop), you could do this:
function delay(t) {
return new Promise(resolve => setTimeout(resolve, t))
}
async function *interval(t) {
while(true) {
let now = Date.now()
yield "hello"
await delay(now - Date.now() + t)
}
}
for await(const greeting of interval(1000)) console.log(greeting)
For interval you can define the interval function like this
function interval() {
return new Promise(function(resolve, reject) {
setInterval(function() {
resolve('Hello');
}, 1000)
})
};
For interval you can use this:
way 1:
interval().then((x) => {
console.log(x);
})
way 2:
const intervalId = setInterval(() => {
interval().then((x) => {
console.log(x);
}, 1000)
})
This is just for stop the interval function after some time.
you must clear the interval if you does not need it more.
setTimeout(() => {
clearInterval(intervalId);
}, 10000);
I'm not sure if this will help but; Any function can be made into a promise and the alternative syntax [async keyword] might be useful for you in this case.
async function test() {
return "hello";
}
test().then( returned => console.log(returned)) // logs hello
setInterval() however does not return a return value rather it returns a "handle".
handle = window . setInterval( handler [, timeout [, arguments ] ] )
...
https://www.w3.org/TR/2011/WD-html5-author-20110705/spec.html#timers
You can, however, make promises from an setinterval ...
interval = window.setInterval(makepromise,1000)
async function makepromise() {
console.log("hello");
}
// or
interval = window.setInterval(async function () {console.log("hello");},1000)
But there is no place for a then then. We are back to callbacks, which we were trying to avoid! But there is functionality perhaps that we can use await within this function.
Better to make your calculateTimeRemaining to a promise And then you can use the then on the interval.
interval = window.setInterval(gameloop,1000);
function gameloop(endDate: string) {
calculateTimeRemaining(endDate: string).then(
//
// my then code goes here.
//
)
}
async calculateTimeRemaining(endDate: string) {
const { secondsInDay, daysOfYear, secondsInHour, secondsInMinute } = this.unitsOfTime;
let distance: number =
(Date.parse(new Date(endDate).toString()) - Date.parse(new Date().toString())) / this.increment;
if (distance > 0) {
// Years left
if (distance >= daysOfYear * secondsInDay) {
// 365.25 * 24 * 60 * 60
this.timeRemaining.years = Math.floor(distance / (daysOfYear * secondsInDay));
distance -= this.timeRemaining.years * daysOfYear * secondsInDay;
}
// Days left
if (distance >= secondsInDay) {
// 24 * 60 * 60
this.timeRemaining.days = Math.floor(distance / secondsInDay);
distance -= this.timeRemaining.days * secondsInDay;
}
// Hours left
if (distance >= secondsInHour) {
// 60 * 60
this.timeRemaining.hours = Math.floor(distance / secondsInHour);
distance -= this.timeRemaining.hours * secondsInHour;
}
// Minutes left
if (distance >= secondsInMinute) {
// 60
this.timeRemaining.minutes = Math.floor(distance / secondsInMinute);
distance -= this.timeRemaining.minutes * secondsInMinute;
}
// Seconds left
this.timeRemaining.seconds = distance;
}
return this.timeRemaining;
}
However, the value of promises is to avoid callback hell with an excessively complex call back scheme ... where the code is calling back, from call backs, from call backs, etc, etc, etc.
Promises do not operate in a 2nd operating system thread like a webworker. So unless you are attempting to clean up callbacks to make code readable or are actually waiting for something there is no benefit to make use of promises.
setInterval is a clean callback. The Gameloop example is not easier to read and understand because a promise was used. I would suggest in this case it is harder to read. at this point ... unless there are other awaits within the loop or a series of promises that do not need to run synchronously;
As already mentioned in the comments that you can not return promises on intervals, but you can keep them in a global object and use later,
const jobs = []
const interval = setInterval(() => {
if(jobs.length == 10) {
clearInterval(interval);
}
let job = Promise.resolve('new job created');
jobs.push(job);
console.log('job created')
}, 1000);
setTimeout(() => {
Promise.all(jobs).then(data => console.log(data))
}, 1000*15);
setInterval already return an integer which is useful to cancel this interval, using clearInterval.
const promise = new Promise((resolve, reject) => {
resolve('Hello');
});
Then use it like
promise.then((result) => {
console.log(result) // Says 'Hello' and will not resolve another value if we call it as it has already been resolved
})
Maybe this is what you tried to achieve.
If you want to call it with an interval of 1000 ms.
const getPromiseInstance = () => new Promise((resolve, reject) => {
resolve(Math.random());
});
setInterval(() => {
getPromiseInstance().then((result) => {
console.log(result)
})
}, 1000)
You should take a look to Observable, maybe it will fit your needs

JS. setInterval does not increment value of variables in external scope

I trying to bring my timer update the value of all let variables. But unfortunally my setInterval fucntion does not want to do this.
Where is my problem located? Thanks
function defineInterval(intervalInHours = 1, precision = 3600, disableInterval = 900, time = 200000) {
let interval = 0
let timeLeftInInterval = 0
let timePercent = 0
let timeDisabled = 0
setInterval(() => {
interval = intervalInHours * precision;
timeLeftInInterval = (time % interval);
console.log(interval, 'interval', timeLeftInInterval, 'timeLeftInInterval ', timeDisabled, 'timeDisabled', timePercent, 'timePercent') // 3600, 2000, 1600, 59 on output
if (disableInterval >= timeLeftInInterval) {
timeDisabled = disableInterval - timeLeftInInterval;
timePercent = (timeDisabled / disableInterval) * 100;
} else {
timeDisabled = interval - timeLeftInInterval;
timePercent = (timeDisabled / (interval++ - disableInterval) * 100);
interval++
}
}, 1000)
}
defineInterval();
Possible issues:
interval = intervalInHours * precision; is getting reset to 3600 every time. Move it above setInterval.
You are not doing anything to change values in if statement, it's not impacting anything. Changing timeDisabled and timePercent is not impacting anything further. It will give same values after every timeout. Are you missing an interval++?
You are doing interval++ two times in your else statement.

Categories

Resources