Why does my timer hook not update it's internal state? - react-hooks

For some reason my timer is not updating it's internal Timer State after I modify the input field. Here is the intial state of my page and State.
This is what my screen and state look like after I modify the input from 10 to 8 seconds. Notice that the Timer State does not update
Here is my code for the workout page:
function WorkoutPage(props: any) {
const DEFAULT_SECONDS_BETWEEN_REPS: number = 10
const [secondsBetweenRepsSetting, setSecondsBetweenRepsSetting] = useState(DEFAULT_SECONDS_BETWEEN_REPS)
const {secondsLeft, isRunning, start, stop} = useTimer({
duration: secondsBetweenRepsSetting,
onExpire: () => sayRandomExerciseName(),
onTick: () => handleTick(),
})
const onTimeBetweenRepsChange = (event: any) => {
const secondsBetweenRepsSettingString = event.target.value;
const secondsBetweenRepsSettingInt = parseInt(secondsBetweenRepsSettingString)
setSecondsBetweenRepsSetting(secondsBetweenRepsSettingInt)
}
return <React.Fragment>
<input type="number" name="secondsBetweenRepsSetting" value={secondsBetweenRepsSetting} onChange={onTimeBetweenRepsChange}/>
</React.Fragment>
}
Here is my useTimer Class:
import { useState } from 'react';
import Validate from "../utils/Validate";
import useInterval from "./useInterval";
export default function useTimer({ duration: timerDuration, onExpire, onTick}) {
const [secondsLeft, setSecondsLeft] = useState(timerDuration)
const [isRunning, setIsRunning] = useState(false)
function start() {
setIsRunning(true)
}
function stop() {
setIsRunning(false)
}
function handleExpire() {
Validate.onExpire(onExpire) && onExpire();
}
useInterval(() => {
const secondsMinusOne = secondsLeft - 1;
setSecondsLeft(secondsMinusOne)
if(secondsMinusOne <= 0) {
setSecondsLeft(timerDuration) // Reset timer automatically
handleExpire()
} else {
Validate.onTick(onTick) && onTick();
}
}, isRunning ? 1000 : null)
return {secondsLeft, isRunning, start, stop, }
}
My full codebase is here in case someone is interested: https://github.com/kamilski81/bdt-coach

Here's the sequence of events you expect:
User changes the input
The change handler fires and calls setSecondsBetweenRepsSetting with the new value
The component re-renders with the new value for secondsBetweenRepsSetting
useTimer is invoked with a duration property of the new value
The secondsLeft state in the useTimer hook changes to the new duration value <-- oops! this does not happen
Why doesn't this last item happen? Because within the useTimer implementation, the only place you use the duration is as the initial value of secondsLeft. Calling the hook a second time with a new duration value will not change the secondsLeft state, and this is by design.
My recommendation would be to include setSecondsLeft in the return value of the useTimer hook to give you a way to override the time left in the timer. You could then use setSecondsLeft directly in the input change handler:
const { secondsLeft, setSecondsLeft, isRunning, start, stop } = useTimer({
duration: secondsBetweenRepsSetting,
onExpire: () => sayRandomExerciseName(),
onTick: () => handleTick(),
});
const onTimeBetweenRepsChange = (event: any) => {
const secondsBetweenRepsSettingString = event.target.value;
const secondsBetweenRepsSettingInt = parseInt(
secondsBetweenRepsSettingString
);
setSecondsBetweenRepsSetting(secondsBetweenRepsSettingInt);
setSecondsLeft(secondsBetweenRepsSettingInt);
};

Related

React - useEffect not using updated value in websocket onmessage

I have a simple issue where a state value updates in my code but is not using the new value. Any ideas what I can do to adjust this?
const [max, setMax] = useState<number>(10);
useEffect(() => {
console.log('max', max); //This outputs correct updated value.
ws.onmessage = (message: string => {
console.log('max', max); //This is always 10.
if (max > 100) {
doSomething(message);
}
}
},[]);
function onChange() {
setMax(1000);
}
<Select onChange={onChange}></Select> //this is abbrev for simplicity
Your useEffect is running only once, on the initial render, using the values from initial render, so max variable is closure captured and not updated in any way. But the solution will be pretty simple, using useRef and one more useEffect to update the ref variable when max variable updates.
const maxRef = useRef(10); // same value
const [max, setMax] = useState(10);
// Only used to update ref variable
useEffect(() => {
maxRef.current = max;
}, [max]);
useEffect(() => {
console.log("max", maxRef.current);
ws.onmessage = (message) => {
console.log("max", maxRef.current);
if (maxRef.current > 100) {
doSomething(message);
}
};
}, []);

The countdown timer goes to negative

My countdown timer won’t stop after 0 and it went to negative even after I clear the interval. I seem not able to see where it went wrong.
Also after the timer goes to 0, I want the page automatically go to the next page without giving specific route, so I’m thinking using useHistory and goForward() but don’t know where I add the hook in this function. Can I return clearInterval and history.goForward() both?
import React, { useEffect, useState } from "react";
import { useHistory } from "react-router-dom";
const Timer = () => {
const [seconds, setSeconds] = useState(10);
const history = useHistory();
useEffect(() => {
const interval = setInterval(
() => setSeconds((prevTimer) => prevTimer - 1),
1000,
);
if (seconds === 0) {
return () => clearInterval(interval);
}
}, []);
return <div className="countdown">{seconds}</div>;
};
export default Timer;
Your useEffect function is only called once on first render and never again. So you don't actually clear the interval. clearInterval needs to sit outside of that function so that it can be called when the seconds reach zero. I would write your code like so:
export default function App() {
const [seconds, setSeconds] = React.useState(10);
const interval = React.useRef();
React.useEffect(() => {
interval.current = setInterval(
() => setSeconds((prevTimer) => prevTimer - 1),
1000
);
}, []);
if (seconds === 0) {
clearInterval(interval.current);
}
return <div className="countdown">{seconds}</div>;
}
useRef is like a “box” that can hold a mutable value in its .current property.
So here you assign the interval to it and can clear it any time you want.
Sandbox
when you set timer inside useEffect it's not aware of changes. I have updated your code. codepen
export default function App() {
let [seconds, setSeconds] = useState(15),
[timer, setTimer] = useState(null); // IN YOU NEED TO STOP TIMER
useEffect(() => {
if (seconds > 0) updateSeconds();
else {
// go to next page
// set timer
// setSeconds(15);
}
// eslint-disable-next-line
}, [seconds]);
/* Timer Logic */
function updateSeconds() {
let timeOut = setTimeout(() => {
setSeconds(seconds - 1);
}, 1000);
setTimer(timeOut);
}
return (
<div className="App">
<h1>Hello Timer</h1>
<h2>{seconds}</h2>
</div>
);
}

Re-execute async RxJS stream after delay

I'm using RxJS 6 to lazily step through iterable objects using code similar to example running below. This is working well but I'm having trouble solving my final use case.
Full code here
import { EMPTY, defer, from, of } from "rxjs";
import { delay, expand, mergeMap, repeat } from "rxjs/operators";
function stepIterator (iterator) {
return defer(() => of(iterator.next())).pipe(
mergeMap(result => result.done ? EMPTY : of(result.value))
);
}
function iterateValues ({ params }) {
const { values, delay: delayMilliseconds } = params;
const isIterable = typeof values[Symbol.iterator] === "function";
// Iterable values which are emitted over time are handled manually. Otherwise
// the values are provided to Rx for resolution.
if (isIterable && delayMilliseconds > 0) {
const iterator = values[Symbol.iterator]();
// The first value is emitted immediately, the rest are emitted after time.
return stepIterator(iterator).pipe(
expand(v => stepIterator(iterator).pipe(delay(delayMilliseconds)))
);
} else {
return from(values);
}
}
const options = {
params: {
// Any iterable object is walked manually. Otherwise delegate to `from()`.
values: ["Mary", "had", "a", "little", "lamb"],
// Delay _between_ values.
delay: 350,
// Delay before the stream restarts _after the last value_.
runAgainAfter: 1000,
}
};
iterateValues(options)
// Is not repeating?!
.pipe(repeat(3))
.subscribe(
v => {
console.log(v, Date.now());
},
console.error,
() => {
console.log('Complete');
}
);
I'd like to add in another option which will re-execute the stream, an indefinite number of times, after a delay (runAgainAfter). I'm having trouble composing this in cleanly without factoring the result.done case deeper. So far I've been unable to compose the run-again behavior around iterateValues.
What's the best approach to accomplish the use case?
Thanks!
Edit 1: repeat just hit me in the face. Perhaps it means to be friendly.
Edit 2: No, repeat isn't repeating but the observable is completing. Thanks for any help. I'm confused.
For posterity here is the full code sample for a revised edition is repeat-able and uses a consistent delay between items.
import { concat, EMPTY, defer, from, interval, of, throwError } from "rxjs";
import { delay, expand, mergeMap, repeat } from "rxjs/operators";
function stepIterator(iterator) {
return defer(() => of(iterator.next())).pipe(
mergeMap(result => (result.done ? EMPTY : of(result.value)))
);
}
function iterateValues({ params }) {
const { values, delay: delayMilliseconds, times = 1 } = params;
const isIterable =
values != null && typeof values[Symbol.iterator] === "function";
if (!isIterable) {
return throwError(new Error(`\`${values}\` is not iterable`));
}
// Iterable values which are emitted over time are handled manually. Otherwise
// the values are provided to Rx for resolution.
const observable =
delayMilliseconds > 0
? defer(() => of(values[Symbol.iterator]())).pipe(
mergeMap(iterator =>
stepIterator(iterator).pipe(
expand(v => stepIterator(iterator).pipe(delay(delayMilliseconds)))
)
)
)
: from(values);
return observable.pipe(repeat(times));
}
I'm gonna be honest, but there could be better solution for sure. In my solution, I ended up encapsulating delay logic in a custom runAgainAfter operator. Making it an independent part, that doesn't affect your code logic directly.
Full working code is here
And the code of runAgainAfter if anybody needs it:
import { Observable } from "rxjs";
export const runAgainAfter = delay => observable => {
return new Observable(observer => {
let timeout;
let subscription;
const subscribe = () => {
return observable.subscribe({
next(value) {
observer.next(value);
},
error(err) {
observer.error(err);
},
complete() {
timeout = setTimeout(() => {
subscription = subscribe();
}, delay);
}
});
};
subscription = subscribe();
return () => {
subscription.unsubscribe();
clearTimeout(timeout);
};
});
};
Hope it helps <3

How long was an observable delayed for when using debounceTime?

In this example: https://rxviz.com/v/0oqKpbWJ the delay in time from the first interval to when a value is emitted from the debounceTime operator is 4 seconds.
Is there a way to know that/be able to log the window that a debounce has debounced for?
Yes, you need timeInterval operator https://rxjs.dev/api/operators/timeInterval
Put it after the debounceTime
Update:
okay, I got it. You need a custom operator for sure. Try this
import { fromEvent, OperatorFunction } from 'rxjs';
import { debounceTime, tap, map } from 'rxjs/operators';
const clicks = fromEvent(document, 'click');
const result = clicks.pipe(debounceTimeWithIntervalTracking(1000));
result.subscribe(x => console.log(x));
function debounceTimeWithIntervalTracking<T>(time: number): OperatorFunction<T, { value: T, delayedFor: number }> {
let startedTime = new Date().getTime();
let restart = true;
return src$ => src$.pipe(
tap(() => {
if (restart) {
startedTime = new Date().getTime();
}
restart = false;
}),
debounceTime(time),
map(value => {
const delayedFor = new Date().getTime() - startedTime;
restart = true;
return { value, delayedFor };
})
)
}

How to restart rxjs interval?

I created a class which sets up a pausable RxJS Observable using the interval operator:
export class RepeatingServiceCall<T> {
private paused = false;
private observable: Observable<T>;
constructor(serviceCall: () => Observable<T>, delay: number) {
this.observable = interval(delay).pipe(flatMap(() => (!this.paused ? serviceCall() : NEVER)));
}
setPaused(paused: boolean) {
this.paused = paused;
}
getObservable() {
return observable;
}
}
This seems to work fine, but the problem I am trying to solve is that I want the timer to reset when unpaused. So, let's say that the interval time is 10 seconds and 5 seconds after the last time the interval emitted, setPaused(false) is called. In that scenario, I want it to emit immediately and then restart the timer.
Would something like that be an easy thing to add?
If you use timer instead of interval, and set the initial delay to 0, then your interval will fire immediately.
You can use takeUntil operator to prevent the interval to run always, and repeat operator with delay option (or repeatWhen for rxjs <7.0) to restart it whenever you want:
import { Observable, Subject, timer } from 'rxjs';
import { repeat, switchMap, takeUntil } from 'rxjs/operators';
export class RepeatingServiceCall<T> {
readonly observable$: Observable<T>;
private readonly _stop = new Subject<void>();
private readonly _start = new Subject<void>();
constructor(serviceCall: () => Observable<T>, delay: number) {
this.observable$ = timer(0, delay)
.pipe(
switchMap(() => serviceCall()),
takeUntil(this._stop),
// repeatWhen(() => this._start) // for rxjs <7.0
repeat({delay: () => this._start}) // for rxjs >7.0
);
}
start(): void {
this._start.next();
}
stop(): void {
this._stop.next();
}
}
Here is a working StackBlitz example.
P.S.: Getters and setters are working different in typescript. So you do not need classic getter concept, you can just make the attribute public and readonly.
You can achieve the behavior you are describing with the following snippet:
const delay = 1000;
const playing = new BehaviorSubject(false);
const observable = playing.pipe(
switchMap(e => !!e ? interval(delay).pipe(startWith('start')) : never())
);
observable.subscribe(e => console.log(e));
// play:
playing.next(true);
// pause:
playing.next(false);
When the playing Observable emits true, the switchMap operator will return a new interval Observable.
Use the startWith operator to emit an event immediately when unpausing.
If you wish to have the interval start automatically when subscribing to the observable, then simply initialize the BehaviorSubject with true.
StackBlitz Example
Yet another approach with a switchMap:
const { fromEvent, timer } = rxjs;
const { takeUntil, switchMap, startWith } = rxjs.operators;
const start$ = fromEvent(document.getElementById('start'), 'click');
const stop$ = fromEvent(document.getElementById('stop'), 'click');
start$.pipe(
startWith(void 0), // trigger emission at launch
switchMap(() => timer(0, 1000).pipe(
takeUntil(stop$)
))
).subscribe(console.log);
<script src="https://unpkg.com/rxjs#6.4.0/bundles/rxjs.umd.min.js"></script>
<button id="start">start</button>
<button id="stop">stop</button>
And a simpler one, that merges start and stop Observables to switch off them:
const { fromEvent, merge, timer, NEVER } = rxjs;
const { distinctUntilChanged, switchMap, mapTo, startWith } = rxjs.operators;
const start$ = fromEvent(document.getElementById('start'), 'click');
const stop$ = fromEvent(document.getElementById('stop'), 'click');
merge(
start$.pipe(mapTo(true), startWith(true)),
stop$.pipe(mapTo(false))
).pipe(
distinctUntilChanged(),
switchMap(paused => paused ? timer(0, 1000) : NEVER)
)
.subscribe(console.log);
<script src="https://unpkg.com/rxjs#6.4.0/bundles/rxjs.umd.min.js"></script>
<button id="start">start</button>
<button id="stop">stop</button>
And another, even wierder approach, using repeat() :
const { fromEvent, timer } = rxjs;
const { take, concatMap, takeUntil, repeat } = rxjs.operators;
const start$ = fromEvent(document.getElementById('start'), 'click');
const stop$ = fromEvent(document.getElementById('stop'), 'click');
start$.pipe(
take(1),
concatMap(()=>timer(0, 1000)),
takeUntil(stop$),
repeat()
).subscribe(console.log);
<script src="https://unpkg.com/rxjs#6.4.0/bundles/rxjs.umd.min.js"></script>
<button id="start">start</button>
<button id="stop">stop</button>
Just wanted to join this party :)
Thanks for #s.alem 's answer, it really helped me.
From official documentation, repeatWhen() is deprecated in RxJs of v7 and will be removed in future version, and repeat() is a replacement of it.
So here's an updated version of #s.alem 's code:
StackBlitz
Basically the change is from
repeatWhen(() => this._start),
to
repeat({ delay: (count) => this._start })
You can abandon the old timer on start and start a new one on start.
const { interval, Subject, fromEvent } = rxjs;
const { takeUntil } = rxjs.operators;
let timer$;
const pause = new Subject();
const obs$ = new Subject();
obs$.subscribe(_ => { console.log('Timer fired') });
function start() {
timer$ = interval(1000);
timer$.pipe(takeUntil(pause)).subscribe(_ => { obs$.next(); });
}
function stop() {
pause.next();
timer$ = undefined;
}
fromEvent(document.getElementById('toggle'), 'click').subscribe(() => {
if (timer$) {
stop();
} else {
start();
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.4.0/rxjs.umd.min.js"></script>
<button id="toggle">Start/Stop</button>
check this code
/**
* it is a simple timer created by via rxjs
* #author KentWood
* email minzojian#hotmail.com
*/
function rxjs_timer(interval, times, tickerCallback, doneCallback, startDelay) {
this.pause = function () {
this.paused = true;
}
this.resume = function () {
this.paused = false;
}
this.stop = function () {
if (this.obs) {
this.obs.complete();
this.obs.unsubscribe();
}
this.obs = null;
}
this.start = function (interval, times, tickerCallback, doneCallback, startDelay) {
this.startDelay = startDelay || 0;
this.interval = interval || 1000;
this.times = times || Number.MAX_VALUE;
this.currentTime = 0;
this.stop();
rxjs.Observable.create((obs) => {
this.obs = obs;
let p = rxjs.timer(this.startDelay, this.interval).pipe(
rxjs.operators.filter(() => (!this.paused)),
rxjs.operators.tap(() => {
if (this.currentTime++ >= this.times) {
this.stop();
}
}),
rxjs.operators.map(()=>(this.currentTime-1))
);
let sub = p.subscribe(val => obs.next(val), err => obs.error(err), () => obs
.complete());
return sub;
}).subscribe(tickerCallback, null, doneCallback);
}
this.start(interval, times, tickerCallback, doneCallback, startDelay);
}
/////////////test/////////////
var mytimer = new rxjs_timer(
1000/*interval*/,
10 /*times*/,
(v) => {logout(`time:${v}`)}/*tick callback*/,
() => {logout('done')}/*complete callback*/,
2000/*start delay*/);
//call mytimer.pause()
//call mytimer.resume()
//call mytimer.stop()
function logout(str){
document.getElementById('log').insertAdjacentHTML( 'afterbegin',`<p>${str}</p>`)
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.1/rxjs.umd.js"></script>
<button onclick="mytimer.pause()"> pause</button>
<button onclick="mytimer.resume()"> resume</button>
<button onclick="mytimer.stop()"> stop</button>
<div id='log'></div>

Resources