useEffect executes with stale information - react-hooks

If I have a useEffect hook like this:
const [text, setText] = useState("");
useEffect(() => {
async function run() {
// fetch new text when some prop changes
const newText = await fetchText();
// to exaggerate the effect of waiting,
// let's sleep for two seconds
await new Promise((r) => setTimeout(r, 2000));
setText(newText);
}
run();
}, [some_prop]);
Every time some_prop changes, I fetch new text from an endpoint. Suppose run takes 3 seconds to finish. If the value of some_prop changes more often that, useEffect will still resolve all the async calls to run. Suppose some_prop changes from a -> b -> c -> d -> e in one second. I would like to see the value of text go smoothly from a to e, but it will flash through all the intermediate values (b -> c -> d) as the calls to run finish. How can I throw away those intermediate calls?

Add a let variable (cancel) to the useEffect block, and if the useEffect is called, set cancel to true. If cancel is called, avoid setting the state.
Note: The obvious solution in the example is to cancel the timeout, but the timeout simulates an api call.
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div id="root"></div>
<script src="https://unpkg.com/#babel/standalone/babel.min.js"></script>
<script type="text/babel">
const { useState, useEffect } = React;
const Demo = () => {
const [num, setN] = useState(0);
const [text, setText] = useState('Text: ');
useEffect(() => {
let cancel = false;
async function run() {
// simulate api call
await new Promise(r => setTimeout(r, 2000));
if(cancel) return;
setText(`Text: ${num * 10}`);
}
run();
return () => {
cancel = true;
};
}, [num]);
return (
<div>
<button onClick={() => setN(n => n + 1)}>{num}</button>
<div>{text}</div>
</div>
);
}
ReactDOM
.createRoot(root)
.render(<Demo />);
</script>
One caveat is that the api is getting called needlessly, and we don't cancel the requests. We can cancel the actual request, by using fetch or any a library that supports cancelling (axios for example).
If you're using fetch, you just need to pass the signel from an abort controller.
fetch(url, { signal })
Read the docs of other libraries to see how they can be cancelled.
This example uses an abort controller to cancel the timeout.
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div id="root"></div>
<script src="https://unpkg.com/#babel/standalone/babel.min.js"></script>
<script type="text/babel">
const { useState, useEffect } = React;
const Demo = () => {
const [num, setN] = useState(0);
const [text, setText] = useState('Text: ');
useEffect(() => {
const controller = new AbortController();
const signal = controller.signal;
async function run() {
// simulate api call
await new Promise(r => {
const timeout = setTimeout(r, 2000);
signal.addEventListener('abort', () => {
clearTimeout(timeout);
});
});
setText(`Text: ${num * 10}`);
}
run();
return () => {
controller.abort();
};
}, [num]);
return (
<div>
<button onClick={() => setN(n => n + 1)}>{num}</button>
<div>{text}</div>
</div>
);
}
ReactDOM
.createRoot(root)
.render(<Demo />);
</script>

Related

React Hook / How to pause & continue my timer?

Hello
I am working on a timer in React to understand how Hooks works, and so far everything is ok except the start button (in my case the timer starts automatically and start button should be use with pause). I can't figure how to resolve this problem with these hooks.
const { useRef, useState, useEffect } = React;
function Minuteur() {
const intervalRef = useRef();
const [timer, setTimer] = useState(30);
useEffect(() => {
const id = setInterval(() => {
setTimer((oldTimer) => oldTimer - 1);
}, 1000);
intervalRef.current = id;
}, []);
const stopTimer = () => {
clearInterval(intervalRef.current);
};
const resetTimer = () => {
setTimer(30)
};
const playTimer = () => {
};
return (
<div>
<p>Il reste : {timer} secondes</p>
<button onClick={playTimer}> PLAY! </button>
<button onClick={stopTimer}> STOP! </button>
<button onClick={resetTimer}> RESET! </button>
</div>
);
Codepen
function Minuteur() {
// Définition de la référence
const intervalRef = useRef();
const [timer, setTimer] = useState(30);
const [timerRunning, setTimerRunning] = useState(false); // I added a state for if the timer should be running or not
useEffect(() => {
let interval = null;
if (timerRunning) { // Check if the timer is running
interval = setInterval(() => {
setTimer(timer => timer - 1);
}, 1000);
}
return () => clearInterval(interval);
}, [timerRunning]); // rerun side effect when timerRunning changes
// Fonction permettant d'arrêter le ‘timer’
const stopTimer = () => {
setTimerRunning(false) // Set running to false
};
const resetTimer = () => {
setTimer(30);
stopTimer();
};
const playTimer = () => {
setTimerRunning(true); // set running to true
};
...
}
Edit: Everything in the [] dependency array at the end of the useEffect hook is what the side effect "watches". So by adding the timerRunning to the dependency array the useEffect hook will watch for the timerRunning and when it changes, it will cause the hook to re-render. If it is an empty array then it will only ever run on the initial load. That is why your timer started on refresh.

How to make a rxjs subscription after a specific action in react hooks?

As we all know that we need to make a subscription in useeffect and unsubscribe it when the component will unmount. But this kind of code will be triggered once the component is mounted. I'm now want to trigger the subscription after a specific action.Look at the code below.
const [timing, setTiming] = useState<number>(60)
const interval$ = interval(1000)
useEffect(() => {
})
const sendCodeOnceSubmit = async (phone: number) => {
const res = await sendCode(phone)
if (res.code !== 200) {
message.error(`${res.message}`)
} else {
interval$.pipe(take(60)).subscribe(() => setTiming(timing - 1))
}
}
I have a form in the dom,and once I click submit,the sendCodeOnceSubmit function will be triggered which will then send a request through sendCode function to the server. Once the server return a success code, I want to make a countdown with rxjs, but how can I unsubscribe it cause the normal way to do it is to subscribe a observable in useeffect. Thanks for anyone who can help.
Just wrap interval$ with useState and write a useEffect for it.
// Moved const out of component.
const defaultTiming = 60;
/* ... */
export default function App() {
const [timing, setTiming] = useState<number>(defaultTiming);
const [interval$, setInterval$] = useState<Observable<number> | undefined>();
useEffect(() => {
if (!interval$) return;
const subscription = interval$.pipe(take(defaultTiming)).subscribe(() => {
setTiming((prev) => prev - 1);
});
return () => subscription.unsubscribe();
}, [interval$]);
const sendCodeOnceSubmit = async (phone: number) => {
const res = await sendCode(phone);
if (res.code !== 200) {
// message.error(`${res.message}`);
console.error(res.message);
} else {
setInterval$(interval(1000));
}
};
return (
<div className="App">
<p>{timing}</p>
<button type="button" onClick={() => sendCodeOnceSubmit(123)}>
Click
</button>
</div>
);
}

React update values

I've got problem with my first react app.
I've set the interval function which counts down from 10 to 0 and after the 0 is reached the interval is cleared. At least it should work like this, but when I console log the time it's always 10 (even though it renders properly in the browser - the value is getting smaller), so it never jumps to the else statement.
What should I do to fix this problem?
const {useState} = React;
const Timer = () => {
let flag = true;
const [time, setTime] = useState(10);
const handleClick = () => {
if (flag) {
setInterval(counter, 500);
}
}
const counter = () => {
if (time > 0) {
console.log(time);
setTime(time => time - 1);
} else {
console.log('out');
clearInterval(timer);
}
}
return(
<div>
<div>{time}</div>
<button className="start" onClick={handleClick}>START</button>
</div>
)
}
ReactDOM.render(
<Timer />,
document.getElementById("react")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="react"></div>
I managed to solve this problem. Thank you guys for trying to help :) Snippet below:
const {useState} = React;
const {useEffect} = React;
const Timer = () => {
const [flag, setFlag] = useState(false);
const [time, setTime] = useState(10);
const handleClick = () => {
setFlag(!flag);
}
useEffect(() => {
function counter () {
if (time > 0) {
setTime(time => time - 1)
}
}
if (flag) {
console.log('a');
const interval = setInterval(counter, 1000)
return () => clearInterval(interval);
}
}, [flag, time]);
return(
<div>
<div>{time}</div>
<button className="start" onClick={handleClick} >START</button>
</div>
)
}
ReactDOM.render(
<Timer />,
document.getElementById("react")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="react"></div>

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>
);
}

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