The context of the problem is the following:
I do a petition to the back and this starts with a slow process that you can check with a rest request.
I need to continue with the main thread when this request indicates that the process is completed.
So I do a request to launch that process, after that, I want to make a request every 20 seconds in order to know if the process is finished and finally I want to continue with the main thread.
In order to solve this I triyed the following code:
...
const checker = await setInterval(() => {
let state = checkState(exportId)
if(state !== '100%'){
state = checkState(exportId)
}else{
clearInterval(checker)
console.log('Clear interval')
}
}, 20000);
...
The check state function is the following:
checkState = async (exportId: string): Promise<IStatus> => {
const headers = {...}
const query = JSON.stringify({...});
const { _, data } = await axios.post(...);
return data
};
The problem with this code is that when I execute it the code inside setInterval never is executed.
So how can I solve this, and await the setInterval until the state return 100%?
Thanks
const checker = await setInterval(() => {
setInterval does not return a promise, it returns an identifier so you can later cancel the interval. Awaiting this value does nothing.
let state = checkState(exportId)
if(state !== '100%'){
checkState is an async function which means it will always return a promise. It will never return a string. await the promise until it resolves to a string:
const checker = setInterval(async () => {
let state = await checkState(exportId);
if(state !== '100%'){
// no need to call checkState because you're using setInterval.
// state = checkState(exportId)
console.log('State currently is', state);
}else{
clearInterval(checker);
console.log('Clear interval');
}
}, 20000);
Or equivalently:
const checker = setInterval(() => {
checkState(exportId).then((state) => {
if(state !== '100%'){
// no need to call checkState because you're using setInterval.
// state = checkState(exportId)
console.log('State currently is', state);
}else{
clearInterval(checker);
console.log('Clear interval');
}
});
}, 20000);
You could wrap the entire thing up in a promise to continue when the main process has finished:
const finished = new Promise((resolve, reject) => {
const checker = setInterval(async () => {
let state = await checkState(exportId);
if(state !== '100%'){
// no need to call checkState because you're using setInterval.
// state = checkState(exportId)
console.log('State currently is', state);
}else{
clearInterval(checker);
resolve();
console.log('Clear interval');
}
}, 20000);
});
await finished;
console.log("Done");
Related
I was wondering if there is any way to break the current process of a UseEffect and have it start on the next render, like this
...
useEffect(() => {
SlowFunction(update);
}, [update]);
setUpdate(1)
// a bit of time passes but not long enough for the SlowFunction(1) to be done
setUpdate(2)
//when this is called and the useEffect runs, stop the SlowFunction(1) and run SlowFunction(2)
my updated personal function is called in the use effect like so,
const [update, setUpdate] = useState(0);
const [thisConst, setThisConst] = useState(0);
async function SlowFunction(firstParam, paramEtc, { signal } = {}) {
while (true) {
//wait two seconds between each
await new Promise((r) => setTimeout(r, 2000));
// Before starting every individual "task" in the function,
// first throw if the signal has been aborted. This will stop the function
// if cancellation occurs:
signal?.throwIfAborted();
// else continue working...
console.log('working on another iteration');
}
console.log('Completed!');
return 'some value';
}
useEffect(() => {
const controller = new AbortController();
const { signal } = controller;
(async () => {
try {
const result = await SlowFunction(update, 'some other value', {
signal,
});
setConst(result);
} catch (ex) {
console.log('EXCEPTION THROWN: ', ex);
}
})();
return () => controller.abort(new Error('Starting next render'));
}, [update]);
The AbortSignal API is the standard method for handling cancellation.
I'll provide an example of how to use it with a function like your SlowFunction. You'll need to accept an abort signal as an optional parameter so that when the next render occurs, the function can be cancelled.
Here's an example cancellable function:
async function SlowFunction (firstParam, paramEtc, {signal} = {}) {
for (let i = 0; i < 1_000_000; i += 1) {
// Before starting every individual "task" in the function,
// first throw if the signal has been aborted. This will stop the function
// if cancellation occurs:
signal?.throwIfAborted();
// else continue working...
console.log('working on another iteration');
}
return 'some value';
}
You can use it in an effect hook like this: returning a cleanup function which invokes the abort method on the controller:
useEffect(() => {
const controller = new AbortController();
const {signal} = controller;
(async () => {
try {
const result = await SlowFunction(update, 'some other value', {signal});
setConst(result);
}
catch (ex) {
// Catch the exception thrown when the next render starts
// and the function hasn't completed yet.
// Handle the exception if you need to,
// or do nothing in this block if you don't.
}
})();
return () => controller.abort(new Error('Starting next render'));
}, [update]);
If the function completes before the next render occurs, then the abort operation will have no effect, but if it hasn't yet, then the next time that the statement signal?.throwIfAborted(); is reached, the function will throw an exception and terminate.
Update in response to your comment:
If your JavaScript runtime is too old to support the AbortSignal.throwIfAborted() method, you can work around that by replacing that line:
signal?.throwIfAborted();
with:
if (signal?.aborted) {
throw signal?.reason ?? new Error('Operation was aborted');
}
I'm building and React application, where I have to save on what page of online display document user currently is, but there is a problem, that if users scroll throughout the document, it saves all pages. So we want to use some kind of timer function, that would only trigger if prop page hasn't changed in 30 seconds for example. Here is my code, it invokes later, but still for all pages through the scroll.
useEffect(
async () => {
let timeout;
if (scriptInfo && authData && numPages) {
setTimeout(async () => {
const res = await postScriptAnalyticsData({
script_id: scriptInfo._id,
user_id: authData.user.user_id,
page: page,
full_page: numPages
});
}, 10000);
}
return () => {
clearTimeout(timeout);
};
},
[ scriptInfo, page, authData, numPages ]
);
Your useEffect() callback function should not be async. The useEffect hook should return a function when a value is returned, but if your callback is async, then it will implicitly return a Promise. Remove the async from your useEffect callback, as this isn't needed as you're not using await directly within the function. Also, assign the timeout to the return value of setTimeout() so you can clear it:
useEffect(() => { // can't be `async`, so remove it
let timeout;
if (scriptInfo && authData && numPages) {
timeout = setTimeout(async () => { // assign `timeout`
const res = await postScriptAnalyticsData({
script_id: scriptInfo._id,
user_id: authData.user.user_id,
page: page,
full_page: numPages
});
}, 10000);
}
return () => {
clearTimeout(timeout);
};
},
[scriptInfo, page, authData, numPages]
);
Declare the setTimeout() ... , out of the components cause or if it is not using hooks
And keep it in a variable
Ex:
const timerCustom = setTimeout(...)
// Component
useEffect (() => {
...
clearTimeout(timerCustom);
...
})
and just clear it when your page props changed
I'm using a useEffect() hook in React to fetch data on an interval every five seconds. When my app first loads, the initial fetch request takes five seconds because it's in the setInterval() function.
I'm trying to make the API call on page load and then every five seconds after that, make API call on the interval to retrieve new data.
What I've tried that's not working:
useEffect(() => {
await updateData(id, state, setState)
.then(() => {
const interval = setInterval(async () => {
if (id) {
await updateData(id, state, setState); // API call
}
}, 5000);
return () => {
clearInterval(interval);
};
},[lensName, state, setState])
}
What I'm currently doing and would like to improve:
useEffect(() => {
// Make API call, once initial call is made and response is returned make calls on a 5 second interval
const interval = setInterval(async () => {
if (id) {
await updateData(id, state, setState); // API call
}
}, 5000);
return () => {
clearInterval(interval);
};
}, [lensName, state, setState])
}
Any help is greatly appreciated.
the important thing to note here is that your updateData function should return a promise to make await work then your above logic will work perfectly. It will wait until the first API call is not finished before going to the second line.
useEffect(() => {
await updateData(id, state, setState);
const interval = setInterval(async () => {
if (id) {
await updateData(id, state, setState); // API call
}
}, 5000);
//update function would be like:
function updateData(id, state, setState) {
...
return API.get("/url");
}
}, []);
You can use time value outside the useEffect hook. Increment it every 5 seconds and pass it as second argument of useEffect. Whenever this time value gets changed, UseEffect will get triggered and it will run the function inside it.
const [timeInterval, setTimeInterval] = useState(0);
setTimeout(() => {
setTimeInterval(timeInterval + 1);
}, 5000);
useEffect(() => {
await updateData(id, state, setState); // API call
}, [timeInterval]);
I would use two useEffect() calls: one for the 5 second poll, and one that fires only once (with an empty dependency array). Something like this:
// Make API call once
useEffect(() => {
const live = true;
if (id) {
await updateData(id, state, setState, live);
}
return () => { live = false; }
}, []);
// Make API call on a 5 second interval
useEffect(() => {
const live = true;
const interval = setInterval(async () => {
if (id) await updateData(id, state, setState, live);
}, 5000);
return () => {
live = false;
clearInterval(interval);
}
}, [lensName, state, setState]);
Also note that you'll want some sort of flag to let your updateData() function know whether the component is still mounted. If it gets unmounted, you don't just want to cancel the interval, you'll also want to avoid calling setState().
You can add another useEffect without dependency to call api when page is first load. However, it's better to show the logic of updateData that we can know what you want to do.
// Call api when first load
useEffect(() => {
await updateData(id, state, setState);
}, [])
// After, every five seconds to call api
useEffect(() => {
const interval = setInterval(async () => {
if (id) {
await updateData(id, state, setState); // API call
}
}, 5000);
return () => {
clearInterval(interval);
};
}, [lensName, state, setState])
The following solution works fine, modify it as per your needs:
function App() {
const [count, setCount] = useState(0);
const myDummyApi = async () => {
for (let i = 0; i < 10 ** 9; i++) {
const val = i;
}
return { data: "some data" };
};
useEffect(() => {
if (count === 0) { // condition for checking if the API call being made is initial one or not.
myDummyApi().then((data) => {
setCount(count + 1);
});
} else {
const timer = setTimeout(() => {
myDummyApi().then((data) => {
setCount(count + 1);
clearTimeout(timer);
});
}, 5000);
}
}, [count]);
return (
<div className="App">
<span>{`Api Call ${count}`}</span>
</div>
);
}
Full code can be found here in sandbox.
Explanation:
make a initial API call
after the promise is resolved set the state and increase the count, then component renders again
after that, as the count > 0 the API call will be made only after timeout of 5 secs.
I have to monitoring some data update info on the screen each one or two seconds.
The way I figured that was using this implementation:
componentDidMount() {
this.timer = setInterval(()=> this.getItems(), 1000);
}
componentWillUnmount() {
this.timer = null;
}
getItems() {
fetch(this.getEndpoint('api url endpoint'))
.then(result => result.json())
.then(result => this.setState({ items: result }));
}
Is this the correct approach?
Well, since you have only an API and don't have control over it in order to change it to use sockets, the only way you have is to poll.
As per your polling is concerned, you're doing the decent approach. But there is one catch in your code above.
componentDidMount() {
this.timer = setInterval(()=> this.getItems(), 1000);
}
componentWillUnmount() {
this.timer = null; // here...
}
getItems() {
fetch(this.getEndpoint('api url endpoint'))
.then(result => result.json())
.then(result => this.setState({ items: result }));
}
The issue here is that once your component unmounts, though the reference to interval that you stored in this.timer is set to null, it is not stopped yet. The interval will keep invoking the handler even after your component has been unmounted and will try to setState in a component which no longer exists.
To handle it properly use clearInterval(this.timer) first and then set this.timer = null.
Also, the fetch call is asynchronous, which might cause the same issue. Make it cancelable and cancel if any fetch is incomplete.
I hope this helps.
Although an old question it was the top result when I searched for React Polling and didn't have an answer that worked with Hooks.
// utils.js
import React, { useState, useEffect, useRef } from 'react';
export const useInterval = (callback, delay) => {
const savedCallback = useRef();
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
const id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
Source: https://overreacted.io/making-setinterval-declarative-with-react-hooks/
You can then just import and use.
// MyPage.js
import useInterval from '../utils';
const MyPage = () => {
useInterval(() => {
// put your interval code here.
}, 1000 * 10);
return <div>my page content</div>;
}
You could use a combination of setTimeout and clearTimeout.
setInterval would fire the API call every 'x' seconds irrespective whether the previous call succeeded or failed. This can eat into your browser memory and degrade performance over time. Moreover, if the server is down, setInterval would continue to bombard the server not knowing its down status.
Whereas,
You could do a recursion using setTimeout. Fire a subsequent API call, only if the previous API call succeed. If previous call has failed, clear the timeout and do not fire any further calls. if required, alert the user on failure. Let the user refresh the page to restart this process.
Here is an example code:
let apiTimeout = setTimeout(fetchAPIData, 1000);
function fetchAPIData(){
fetch('API_END_POINT')
.then(res => {
if(res.statusCode == 200){
// Process the response and update the view.
// Recreate a setTimeout API call which will be fired after 1 second.
apiTimeout = setTimeout(fetchAPIData, 1000);
}else{
clearTimeout(apiTimeout);
// Failure case. If required, alert the user.
}
})
.fail(function(){
clearTimeout(apiTimeout);
// Failure case. If required, alert the user.
});
}
#AmitJS94, there's a detailed section on how to stop an interval that adds onto the methods that GavKilbride mentioned in this article.
The author says to add a state for a delay variable, and to pass in "null" for that delay when you want to pause the interval:
const [delay, setDelay] = useState(1000);
const [isRunning, setIsRunning] = useState(true);
useInterval(() => {
setCount(count + 1);
}, isRunning ? delay : null);
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
Definitely read the article to get a better understanding of the details -- it's super thorough and well-written!
As Vasanth mention, I preferred to:
use setTimeout to measure the time between the end of the last request and the beginning of the next one
make the first request straight away, not after the delay
inspired by the answer from #KyleMit https://stackoverflow.com/a/64654157/343900
import { useEffect, useRef } from 'react';
export const useInterval = (
callback: Function,
fnCondition: Function,
delay: number,
) => {
const savedCallback = useRef<Function>();
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
let id: NodeJS.Timeout;
const tick = async () => {
try {
const response =
typeof savedCallback.current === 'function' &&
(await savedCallback.current());
if (fnCondition(response)) {
id = setTimeout(tick, delay);
} else {
clearTimeout(id);
}
} catch (e) {
console.error(e);
}
};
tick();
return () => id && clearTimeout(id);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [delay]);
};
WORKS: Using fnCondition inside which can be a condition based on the response from the last request.
//axios-hooks
const {
data,
isLoadingData,
getData,
} = api.useGetData();
const fnCondition = (result: any) => {
const randomContidion = Math.random();
//return true to continue
return randomContidion < 0.9;
};
useInterval(() => getData(), fnCondition, 1000);
DOES NOT WORK: Passing delay as null to stop useInterval like this does not work for me
with this code: https://www.aaron-powell.com/posts/2019-09-23-recursive-settimeout-with-react-hooks/
(You might get the impression it works, but after a few starts/stops it breaks)
const [isRunning, setIsRunning] = useState(true);
const handleOnclick = () => {
setIsRunning(!isRunning);
};
useInterval(() => getData(), isRunning ? 1000 : null);
<button onClick={handleOnclick}>{isRunning ? 'Stop' : 'Start'}</button>
Sum up: I'm able to stop useInterval by passing fnCondition, but not by passing delay=null
Here's a simple, full solution, that:
Polls every X seconds
Has the option of increasing the timeout each time the logic runs so you don't overload the server
Clears the timeouts when the end user exits the component
//mount data
componentDidMount() {
//run this function to get your data for the first time
this.getYourData();
//use the setTimeout to poll continuously, but each time increase the timer
this.timer = setTimeout(this.timeoutIncreaser, this.timeoutCounter);
}
//unmounting process
componentWillUnmount() {
this.timer = null; //clear variable
this.timeoutIncreaser = null; //clear function that resets timer
}
//increase by timeout by certain amount each time this is ran, and call fetchData() to reload screen
timeoutIncreaser = () => {
this.timeoutCounter += 1000 * 2; //increase timeout by 2 seconds every time
this.getYourData(); //this can be any function that you want ran every x seconds
setTimeout(this.timeoutIncreaser, this.timeoutCounter);
}
Here is a simple example using hooks in function component and this will refresh your data in a set interval.
import React from 'react';
import { useEffect, useState } from 'react';
export default function App() {
let [jokes, setJokes] = useState('Initial');
async function fetchJokes() {
let a = await fetch('https://api.chucknorris.io/jokes/random');
let b = await a.json();
setJokes(b.value);
}
// Below function works like compomentWillUnmount and hence it clears the timeout
useEffect(() => {
let id = setTimeout(fetchJokes, 2000);
return () => clearTimeout(id);
});
return <div>{jokes}</div>;
}
or, you can use axios as well to make the API calls.
function App() {
const [state, setState] = useState("Loading.....");
function fetchData() {
axios.get(`https://api.chucknorris.io/jokes/random`).then((response) => {
setState(response.data.value);
});
}
useEffect(() => {
console.log("Hi there!");
let timerId = setTimeout(fetchData, 2000);
return ()=> clearInterval(timerId);
});
return (
<>
This component
<h3>{state}</h3>
</>
);
}
I'm new to Angular 2 so maybe this is trivial - I'm trying to figure out how to cancel an Rx.Observable interval call that happens every 2 seconds. I subscribe to the Rx.Observable with this:
getTrainingStatusUpdates() {
this.trainingService.getLatestTrainingStatus('train/status')
.subscribe(res => {
this.trainingStatus = res;
if (this.trainingStatus == "COMPLETED") {
//Training is complete
//Stop getting updates
}
});
}
This is how my Rx.Observable interval call is handled in my service.ts file (this is called by the function above from a different file):
getLatestTrainingStatus(url: string) {
return Rx.Observable
.interval(2000)
.flatMap(() => this._http.get(url))
.map(<Response>(response) => {
return response.text()
});
}
As you can see in the subscription method, I simply want to stop the interval calls (every 2 seconds) when 'trainingStatus' is 'COMPLETED'.
That's possible with the unsubscribe option.
Every .subscribe() returns a subscription object. This object allows you to unsubscribe to the underlying Observable.
getTrainingStatusUpdates() {
// here we are assigning the subsribe to a local variable
let subscription = this.trainingService.getLatestTrainingStatus('train/status')
.subscribe(res => {
this.trainingStatus = res;
if (this.trainingStatus == "COMPLETED") {
//this will cancel the subscription to the observable
subscription.unsubscribe()
}
});
}
getTrainingStatusUpdates() {
this.trainingService.getLatestTrainingStatus('train/status')
.do(res => this.trainingStatus = res)
.first(res => res == "COMPLETED")
.subscribe(/* Do stuff, if any */);
}
You don't have to unsubscribe / stop the interval. It will be stopped when res == "COMPLETED"