Refreshing and updating useState/useRef in React - javascript

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.

Related

Why can't I change this global variable from function? (JavaScript)

Simple countdown project.
Desired outcome:
global variable determines default time.
then slider value overrides that variable.
PROBLEM: slider value changes locally but global value stays the same.
I've watched tutorials all day on variable scopes and still don't see what's wrong because:
Global is declared with "var" outside function: var initialMinutes = 5
attempt to update value inside function looks like this:
const change = slider.addEventListener('change', setMins)
function setMins() {
initialMinutes = slider.value
}
I have also tried with window.variable here and there to no avail.
I hope someone can help. This is becoming difficult to scope with.
const countdown = document.getElementById('countdown')
const slider = document.getElementById('slider')
const startBtn = document.getElementById('btn-startStop')
const resetBtn = document.getElementById('btn-reset')
// Event Listeners
const change = slider.addEventListener('change', setMins)
const start = startBtn.addEventListener('click', startStop)
const reset = resetBtn.addEventListener('click', resetApp)
// Time
var initialMinutes = 15
countdown.innerHTML = initialMinutes+':00'
const initialDuration = initialMinutes * 60
let time = initialMinutes * 60
let interval = null
let status = 'stopped'
function updateCountdown() {
const minutes = Math.floor(time / 60)
let seconds = time % 60
seconds = seconds < 10 ? '0'+ seconds : seconds
if( time < 1) {
clearInterval(interval)
startBtn.innerHTML = 'START'
status = 'stopped'
countdown.innerHTML = `00:00`
} else {
countdown.innerHTML = `${minutes}:${seconds}`
time--;
}
}
function startStop() {
if(status === 'stopped') {
interval = setInterval(updateCountdown, 50)
startBtn.innerHTML = 'STOP'
status = 'running'
} else {
clearInterval(interval)
startBtn.innerHTML = 'START'
status = 'stopped'
}
}
function setMins() {
initialMinutes = slider.value
countdown.innerHTML = slider.value+':00'
}
function resetApp() {
clearInterval(interval);
document.getElementById('countdown').innerHTML = '00:00'
startBtn.innerHTML = 'START'
status = 'stopped'
}
Codepen link included for clarity:
https://codepen.io/donseverino/pen/YzWBJYV
Got it! It was not a problem of scope but of variable assignment.
updateCountdown() uses time and time = initialMinutes * 60
I thought changing initialMinutes would automatically change its value inside time but it doesn't.
Reassigning time inside the function solves it.
function setMins() {
initialMinutes = slider.value
time = initialMinutes * 60
}

setInterval updating state in React but not recognizing when time is 0

I am practicing React useState hooks to make a quiz timer that resets every ten seconds. What I have now is updating the state each second, and the p tag renders accordingly. But when I console.log(seconds) it shows 10 every time, and so the condition (seconds === 0) is never met . In Chrome's React DevTools, the state is updating accordingly as well. What am I doing wrong here?
import React, {useState } from 'react';
function App() {
const [seconds, setSeconds] = useState(10);
const startTimer = () => {
const interval = setInterval(() => {
setSeconds(seconds => seconds - 1);
// Logs 10 every time
console.log(seconds)
// Never meets this condition
if (seconds === 0) {
clearInterval(interval)
}
}, 1000);
}
return (
<div>
<button onClick={() => startTimer()}></button>
// updates with current seconds
<p>{seconds}</p>
</div>
)
}
export default App;
That is because the setSeconds updates the state with a new variable on every tick but the initial reference (seconds === 10) still points to the initial set variable. That is why it stays at 10 => The initial reference.
To get the current value, you have to check it in the setSeconds variable (passed as seconds) like this:
const startTimer = () => {
const interval = setInterval(() => {
setSeconds(seconds => {
if(seconds < 1) {
clearInterval(interval);
return 10;
}
return seconds - 1;
});
}, 1000);
}
Here is a working sandbox
You should also rename the variable to not have the same name (seconds) twice in a single funtion.

useAnimationOnDidUpdate react hook implementation

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.

How to synchronize a countdown between a Swing-GUI and a WebApplication like Vaadin or native JS

Iam trying to synchronize a Countdown from a Java Application to a Browser. The Countdown can be stopped, started and resetted anytime.
I have tried to implement this in Vaadin 13 but cant access the the UI Access Mehtod to lock the vaadin session. Now Iam trying to implement this with native JS and Ajax requests but iam not sure how to synchronize the stop/start and reset events without making an ajax request every second.
This is the Swing Implementation of the counter
public void timer() {
Timer timer = new Timer(1000, new ActionListener() {
public void actionPerformed(ActionEvent e) {
if (seconds == 0 && minutes > 0) {
minutes--;
seconds = 59;
} else {
seconds--;
}
label.setText(minutes+":"+seconds);
repaint();
}
});
timer.start();
}
Now I would offer a Spring Boot Rest API for the JS Code to ask for minutes and seconds remaining.
setInterval(test, 1000);
async function test() {
var xhttp = new XMLHttpRequest();
xhttp.open("GET", "http://10.0.1.17/countdown", false);
xhttp.send();
//console.log(JSON.parse(xhttp.responseText));
//Do Something with it
}
This seems to be an unreliable and unefficient way
Check this class out, from another answer I wrote
/*
a (pausable) linear equation over real time
value = _speed * Date.now() + _offset;
//when paused, it's simply:
value = _offset;
so basically a clock, a stopwatch, a countdown, a gauge, ...
since it is only a linear equation over time, it is independant of any interval.
It computes the value (using Date.now()) whenever you ask for it. Wether this is ever frame or every hour.
*/
class Clock {
constructor(value=Date.now(), speed=1){
//state; changes only when YOU set one of the properties (value, paused or speed)
this._offset = +value || 0;
this._speed = +speed || 0;
this._paused = true;
//preparing a simple hook to get notified after the state has been updated (maybe to store the new state in the localStorage)
this.onStateChange = undefined;
}
get value(){
return this._paused? this._offset: this._speed*Date.now() + this._offset
}
set value(arg){
let value = +arg || 0;
let offset = this._paused? value: value - this._speed * Date.now();
if(this._offset !== offset){
this._offset = offset;
if(typeof this.onStateChange === "function")
this.onStateChange(this);
}
}
get speed(){
return this._speed
}
set speed(arg){
let speed = +arg || 0;
if(this._speed !== speed){
if(!this._paused)
this._offset += Date.now() * (this._speed - speed);
this._speed = speed;
if(typeof this.onStateChange === "function")
this.onStateChange(this);
}
}
get paused(){
return this._paused
}
set paused(arg){
let pause = !!arg;
if(this._paused !== pause){
this._offset += (pause? 1: -1) * this._speed * Date.now();
this._paused = pause;
if(typeof this.onStateChange === "function")
this.onStateChange(this);
}
}
time(){
let value = this.value,v = Math.abs(value);
return {
value,
//sign: value < 0? "-": "",
seconds: Math.floor(v/1e3)%60,
minutes: Math.floor(v/6e4)%60,
hours: Math.floor(v/36e5)%24,
days: Math.floor(v/864e5)
}
}
valueOf(){
return this.value;
}
start(){
this.paused = false;
return this;
}
stop(){
this.paused = true;
return this;
}
}
I show this because if you take a closer look at it you'll see that the entire state of this thing consists of two numbers and a boolean and they only change when you do something, like start/stop it.
The actual value is computed from this state and the Computers internal clock.
So, if you synchronize this state between frontend and backend, they both run (mostly) in sync.
Why mostly? Because of the little delay before the other end has recieved the new state. For these few ms the two are out of sync. As soon as the other end has updated its state, they're again in sync.

Changing the interval time when one of the states' value is changed

someFunction() {
//........ some stuff
else {
const getData = setInterval(() => {
this.sendRequest(id);
if (this.state.calls === this.state.selectedItems.length) {
this.setState({ done: true });
clearInterval(getData);
}
}, this.changeInterval());
}
},
changeInterval() {
if(this.state.processed === true) {
return 1000;
} else {
return parseInt(this.state.userSetValue, 10) * 1000 + 1000;
}
},
sendRequest(){
//........ some stuff
let {calls, processed} = this.state;
if(!processed){
processed = true;
}
calls++;
this.setState({calls, processed});
//........ some stuff
}
As i understand, the setInverval() function doesn't check its time interval whether it has changed or not after running function this.sendRequest(id).
My problem is that I have to run sendRequest() after waiting some time, and that some time can be changed dynamically in my app. Currently, it only runs in an interval when it's FIRST set.
How could I change the interval of setInterval() each time this.sendRequest(id) is executed ?
UPDATE
I've made some changes to someFunction():
someFunction() {
//........ some stuff
else {
let delay = parseInt(this.state.userSetValue, 10) * 1000 + 1000;
let runFunction = () => {
if (this.state.processed) {
delay = 1000;
}
// if (this.state.calls === this.state.selectedItems.length) {
// this.setState({ done: true });
// clearTimeout(runFunction);
// }
this.sendRequest();
setTimeout(runFunction, delay);
}
setTimeout(runFunction, delay);
}
}
The problem with this now is:
1) The code keeps running infinitely without stopping (the commented out if statement didn't make it stop)
2) I'd like the very first call to this.sendRequest() get executed after parseInt(this.state.userSetValue, 10) * 1000 + 1000 time and the rest consecutive calls executed after 1000 ms.
I have to call the this.sendRequest() this.state.selectedItems.length number of times. The very first call always has a delay ofparseInt(this.state.userSetValue, 10) * 1000 + 1000 ms while the rest have 1000 ms.
Could anyone show an example how to change the timer interval after the first calling some function ?
One of way of doing it would be to use setTimeout() instead of setInterval(). The basic ideas is as follows:
function doWork() {
// Do your work here
// Schedule more work with a new delay
var newDelay = 2000;
doWorkAfterDelay(newDelay)
}
function doWorkAfterDelay(delay) {
setInterval(doWork, delay)
}
var intialDelay = 1000;
doWorkAfterDelay(intialDelay);

Categories

Resources