so I have a function that runs every 5 seconds. Inside this function, I check if the state is null, to then set a value to it. The problem is that, every time the function run, it detects the state as null, even tho it is not null.
My code:
const [activeChat, setActiveChat] = useState(null)
const loadChats = async () => {
await api.get('/v1/chat/chats')
.then((res) => {
if (activeChat === null) {
if (res.data.chats.length > 0) {
setActiveChat(res.data.chats[0])
}
}
setChats(res.data.chats)
setLoading(false)
})
.catch((err) => {
setLoading(false)
})
}
useEffect(() => {
loadChats()
let interval = setInterval(() => {
loadChats()
}, 5000);
return () => clearInterval(interval)
}, [])
the activeChat should be only set on the first load, if its not set yet, but it keeps detecting as null every time the function runs. Why does it keep detecting as null?
Obs: As I said, the state is really being set, as expected, so the problem is not with the response or something, i don't know what is happening..
Inside setInterval or setTimeout, the state will NOT be changed even you have changed it inside the setInterval function. Try to create a timer using setInterval and you can see the state does not change inside it.
const [timer, setTimer] = useState(0);
useEffect(() => {
setInterval(() => {
console.log(timer); // remains the same (0) forever
setTimer(timer + 1);
}, 1000);
}, []);
useEffect(() => {
console.log(timer); // this one should only change from 0 to 1, because timer always being set as 0 + 1;
}, [timer]);
You can create a ref for the chat object only for updating, and a state only for the chat display. Use the ref to keep track of the active chat, while using the state for UI display.
Related
I am trying to build a cryptocurrency application. But I am having trouble getting populated state values that are passed into the useEffect as parameters to a debouncing/polling function.
The issue is that the debouncing works well, as in it detects the value and calls the api after the 500ms that I specified in debounce. However, the polling portion seems to not have the state values of of transactionType, fromCurrencyAmount, and pair. It seems like after I debounce the input, after 6 seconds the polling will do its thing but the values passed in the params are undefined. Is there anyway I can solve this?
Here is the method that serves two purposes. It has an api to be polled from every 6 seconds, as well as getting debounced input if the user enters an amount inside the input.
function handleInitPoll(baseAndQuote, side, value) {
getSwapPrice(baseAndQuote, side, value || 0)
.then((res) => {
if (!res.price) {
setIsLoading(true);
} else if (res.error) {
setErrorMessage(res.error);
} else if (res.price) {
setIsLoading(false);
setSwapPriceInfo(res);
}
});
}
And here is the useEffect:
useEffect(() => {
handleInitPoll(pair, transactionType, fromCurrencyAmount);
const timer = setInterval(handleInitPoll, 6000, pair, transactionType, fromCurrencyAmount);
return () => {
clearInterval(timer);
};
}
setSelectedCurrencyState({ ...selectedCurrencyState, selectedFromCurrency: 'USDT', selectedToCurrency: 'XLM' });
}, [pair, transactionType, fromCurrencyAmount]);
And here is the debounce declaration:
const debounceOnChange = useCallback(debounce(handleInitPoll, 500, pair, transactionType, fromCurrencyAmount), []);
And here is where the debouncing is being done, which is inside an onChange handler:
const handleAssetAmount = (e) => {
const { value } = e.target;
const formattedAmount = handleAssetAmountFormat(value);
setFromCurrencyAmount(formattedAmount);
validateInputAmount(formattedAmount);
debounceOnChange(pair, transactionType, formattedAmount);
};
Issue
The issue here is that you've closed over stale values in the interval callback.
Solution
One solution is to cache these state values in a React ref such that the current value can be accessed in the polling function.
Example:
const pairRef = React.useRef(pair);
const transactionTypeRef = React.useRef(transactionType);
const fromCurrencyAmountRef = React.useRef(fromCurrencyAmount);
useEffect(() => {
pairRef.current = pair;
}, [pair]);
useEffect(() => {
transactionTypeRef.current = transactionTypeRef;
}, [transactionType]);
useEffect(() => {
fromCurrencyAmountRef.current = fromCurrencyAmount;
}, [fromCurrencyAmount]);
useEffect(() => {
handleInitPoll(pair, transactionType, fromCurrencyAmount);
const timer = setInterval(() => {
handleInitPoll(
pairRef.current,
transactionTypeRef.current,
fromCurrencyAmountRef.current
);
}, 6000);
return () => {
clearInterval(timer);
};
}, [pair, transactionType, fromCurrencyAmount]);
Fundamentally, your code seems to be correct with a few issues:
There is a race condition.
If getSwapPrice is running and the component is updated, it can still
affect the state when setSwapPriceInfo or setLoading are called when
the promise is resolved.
This is particularly bad, because network requests can "overtake" each
other. Thus it can happen that the return value of getSwapPrice
updates the component with the result of an old network request.
This is discussed in this article.
There is this odd call to setSelectedCurrencyState in the useEffect
block. It's not clear what this is supposed to do, but it clearly doesn't
belong there.
However, the underlying application should work fine, I reproduced it with a simpler application here:
import { useEffect, useState } from "react";
function fetchExchangeRateAsync(multiplier) {
return new Promise(resolve => {
setTimeout(() => {
resolve(Math.random() * multiplier);
}, 100);
});
}
function CurrencyExchangeRate() {
const [exchangeRate, setExchangeRate] = useState(null);
const [inputValueString, setInputValueString] = useState("");
const inputValue = Number(inputValueString);
const [multiplier, setMultiplier] = useState(1.0);
let outputValue = null;
if (!isNaN(inputValue) && exchangeRate !== null) {
outputValue = inputValue * exchangeRate;
}
useEffect(() => {
// To avoid race conditions, we must not update the state from an asynchronous operation if
// the component was re-rendered since then.
//
// https://overreacted.io/a-complete-guide-to-useeffect/
let didCancel = false;
// Do not delay the first request.
fetchExchangeRateAsync(multiplier)
.then(newExchangeRate => {
if (!didCancel) {
setExchangeRate(newExchangeRate);
}
});
// Poll exchange rate.
let intervalHandle = setInterval(() => {
fetchExchangeRateAsync(multiplier)
.then(newExchangeRate => {
if (!didCancel) {
setExchangeRate(newExchangeRate);
}
});
}, 500);
return () => {
didCancel = true;
clearInterval(intervalHandle);
};
}, [multiplier]);
return (
<div>
<input value={inputValueString} onChange={event => setInputValueString(event.target.value)} /><br />
<p>With current exchange rate: {outputValue !== null ? outputValue : "(loading)"}</p>
<button onClick={() => setMultiplier(100.0)}>Set Multiplier</button>
</div>
);
}
function App() {
return (
<CurrencyExchangeRate />
);
}
export default App;
This is quite a bit different from what you are doing but it does demonstrate that your code should generally work:
It is possible to trigger the fetch logic by changing the input field (here without debouncing) or in a given interval.
The fetch logic runs immediately when the component is rendered for the first time.
The "Set Multiplier" button can affect the value of multiplier and this information arrives in the setInterval call correctly.
This works because [multiplier] dictates that the effect should be re-run if that variable changes. When this happens, the old interval is first cleared with clearInterval and then re-started with setInterval.
In your case that would be pair, transactionType and fromCurrencyAmount instead of multiplier.
In other words, your issue seems to be outside the code that you provided in the question.
Most of the answers I found here was to use clearInterval() inside a return statement in a useEffect(). But, still for some reasons it keeps executing.
I'm also getting the following warning in the logs :-
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
in StartTest (at SceneView.tsx:126)
attaching the code for Reference.
const [connectionInterval, setConnectionInterval] = useState(null);
const [batteryInterval, setBatteryInterval] = useState(null);
const [heartRateInterval, setHeartRateInterval] = useState(null);
useEffect(() => {
startServices();
return () => {
clearServices();
};
}, []);
const startServices= () => {
let ctnInt = setInterval(() => checkConnection(), 5000);
setConnectionInterval(ctnInt);
let btryInt = setInterval(
() =>
Battery(value => {
setBattery(value);
}),
900000,
);
setBatteryInterval(btryInt);
let hrRtInt = setInterval(
() =>
HeartRate(
hr => {
if (finish) {
clearInterval(heartRateInterval);
}
let rate = Math.round(hr);
setHeartRate(rate);
},
onError => {
console.log('API ERROR');
},
),
3000,
);
setHeartRateInterval(hrRtInt);
};
const clearServices = () => {
clearInterval(connectionInterval);
clearInterval(batteryInterval);
clearInterval(heartRateInterval);
};```
You're not passing any deps to useEffect, so the effect functions never update, and in the version of clearServices that you call, connectionInterval and friends are all still null. See the note here.
In general I would approach setInterval like this:
useEffect(() => {
const intervalFn = () => {
console.log('interval fired')
}
const intervalId = setInterval(intervalFn, 1000)
return () => {
clearInterval(intervalId)
}
}, [])
(this version really has no deps, because everything is captured inside useEffect. but in practice you would probably have some.)
I had this issue a couple of weeks ago and what I did to stop it was to set a new state after clearing the interval.
For example. I was trying to build a countdown that ran from 30 to 0 and stop at 0. This is what I did
const [timeRemaining, setTimeRemaining] = useState(30);
useEffect(() => {
let timeLeft = timeRemaining;
let interval = setInterval(() => {
if (timeLeft === 0) {
clearInterval(interval);
setTimeRemaining(0);
} else {
setTimeRemaining((timeLeft -= 1));
}
}, 1000);
}, [timeRemaining]);
Setting the state to 0 after clearing the interval was the only way to stop the countdown timer at 0
In useEffect, you should declare dependencies in array after callback function. In the code above, startServices is dependency, because it is declared outside the useEffect.
https://reactjs.org/docs/hooks-reference.html#useeffect
You can learn about useEffect in link.
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>
</>
);
}