Where to put setInterval in react - javascript

I need every 5 seconds (actually every 2 minutes, but having 5 seconds while testing) to make a GET request to an API.
This should be done only when the user is logged in.
The data should be displayed by one component that is called "Header".
My first thought was to have an interval inside that component.
So with "useEffect" I had this in the Header.js:
const [data, setData] = useState([]);
useEffect(async () => {
const interval = setInterval(async () => {
if(localStorage.getItem("loggedInUser")){
console.log("Logged in user:");
console.log(new Date());
try{
const data = await getData();
if(data){
setData(data);
}
console.log("data",data);
}
catch(err){
console.log("err",err);
}
}
}, 5000);
return () => clearInterval(interval);
}, []);
Im getting the data. But looking at the console, it does not look so good.
First of all, sometimes I get this:
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.
It seems to happen when I go to other pages that have been called with a "href" (so without using for example history to change page).
Also, probably because of the thing above, after a while that I browse the pages, I see that the interval is not every 5 seconds anymore. It happens 2-3 times every 5 seconds. Maybe every seconds.
To me this does not look good at all.
Im wondering first of all if this component is the right place to put the interval in.
Maybe the interval should be in App.js? But then I would need a mechanism to pass data to children component (the Header.js, maybe could do this with Context).

As Sergio stated in his answer on async actions you should aways check if the component is currently mounted before doing side effects.
However using a variable that is initialised within the component won't work as it is being destroyed on unmount. You should rather use useRef hook as it will persist between component mounts.
const [data, setData] = useState([]);
const mountedRef = useRef(null);
useEffect(() => {
mountedRef.current = true;
return () => {
mountedRef.current = false;
};
}, []);
useEffect(async () => {
const interval = setInterval(async () => {
if (localStorage.getItem("loggedInUser")){
console.log("Logged in user:", new Date());
try {
const data = await getData();
if (data && mountedRef.current) {
setData(data);
console.log("data", data);
}
} catch(err) {
if (mountedRef.current) console.log("err", err);
}
}
}, 5000);
return () => clearInterval(interval);
}, []);
Or alternatively declare a boolean variable in the module scope so that its not freed by the garbage collection process

You can create a isMounted variable to control if the async task happens after the component is unmounted.
As the error said, you are trying to update component state after it's unmounted, so you should "protect" the setData call.
const [data, setData] = useState([]);
useEffect(async () => {
let isMounted = true;
const interval = setInterval(async () => {
if(localStorage.getItem("loggedInUser")){
console.log("Logged in user:");
console.log(new Date());
try{
const data = await getData();
if(data && isMounted){
setData(data);
}
console.log("data",data);
}
catch(err){
console.log("err",err);
}
}
}, 5000);
return () => {
clearInterval(interval);
isMounted = false;
}
}, []);

Related

Is there a better way to organize this React page with an async call to the API?

I have this useEffect that should only run once. Hence I have set router.query.something as a dependency. The request must wait for router.query.something to be hydrated, then it only makes the request if the value is truthy.
This just seems like an overly-complicated function to make a simple one-time request to the API, but I haven't figured out a cleaner way to do it.
const MyPage = () => {
const [user, setUser] = useState(null)
const router = useRouter()
useEffect(async () => {
try {
if (router.query.something) {
const res = await api.get(`/resetTokens/${router.query.something}`)
setUser(res.data)
}
} catch (err) {
setUser(null)
}
}, [router.query.something])
return (<div>...</div>)
}
Normally, I would use getServerSideProps, but in this situation, I can't, because the request is setting a cookie. If I use getServerSideProps the cookie doesn't pass through to the client.
You should note two things.
First, useEffect should not be using async functions. There should be a warning in the console regarding your use of async in useEffect.
Secondly, you should be cleaning up the useEffect since you are dealing with promises. If you do not clean up you may encounter memory leaks which can cause unexpected results.
Your code should look something like this instead
useEffect(() => {
let isMounted = true;
if (router.query.something) {
api.get(`/resetTokens/${router.query.resetToken}`)
.then(res=> {
if(!isMounted) return;
setUser(res.data)
})
.catch(e => {
if(!isMounted) return;
setUser(null)
});
}
return () => {
isMounted = false;
};
}, [router.query.something])
You could also try doing this with AbortController. Academind has a good blog explaining what is happening and how to fix. https://academind.com/tutorials/useeffect-abort-http-requests/
const MyPage = () => {
const [user, setUser] = useState(null)
const router = useRouter()
useEffect(() => {
const abortController = new AbortController();
const fetchData = async () => {
try {
const res = fetch(`/resetTokens/${router.query.resetToken}`, { signal: abortController.signal });
setUser(res.data)
} catch (e) {
setUser(null)
}
};
if (router.query.something) {
fetchData();
}
return () => {
abortController.abort();
};
}, [router.query.something]);
return (<div>...</div>)
}

React.js useEffect with nested async functions

I have the common warning displaying upon loading of my web app but never again...
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.
EDIT****
It is caused by this chunk of code. I have narrowed it down to one function. It blows up when I try to setMoisture state. I am not sure why.
function getData (){
Axios.get("http://localhost:3001/api/get-value").then((response) => {
const recievedData = response.data;
const dataValue = recievedData.map((val) => {
return [val.value]
})
if (loading === true){
setLoading(false);
}
return parseInt(dataValue);
}).then((resp)=>setMoisture(resp))
}
React.useEffect(() => {
if (moisture === "initialState"){
getData();
}
}, []);
Posting the answer here (based from the comments) for completeness.
Basically, use local variables and cleanup function towards the end of useEffect(). Using this as reference:
Similar situation here
You should declare the function inside the useEffect or add it as a dependency. one way to do it's just moving your function inside the hook.
// I assumed that your useState hooks looks something similar.
const [moisture, setMoisture] = React.useState('initialState')
const [loading, setLoading] = React.useState(true)
React.useEffect(() => {
function getData() {
Axios.get("http://localhost:3001/api/get-value").then((response) => {
const recievedData = response.data;
const dataValue = recievedData.map((val) => {
return [val.value]
})
if(loading === true) {
setLoading(false);
}
return parseInt(dataValue);
}).then((resp => setMoisture(resp)))
}
if (moisture === "initialState"){
getData();
}
}, [])
You also probably want to first set your data to the state and then change your loading state to false, this is gonna prevent some bugs. This is another way to do it and manage the loading state and the promises
React.useEffect(() => {
function getData() {
setLoading(true)
Axios.get("http://localhost:3001/api/get-value")
.then((response) => {
const dataValue = response.data.map((val) => {
return [val.value]
})
// This is going to pass 0 when can't parse the data
setMoisture(parseInt(dataValue) || 0)
setLoading(false)
})
}
getData()
}, [])

Too frequent API fetch with setInterval() in React

I have a problem with my React project. I have a basic SelectList with milli seconds and based on that I want to set the interval of setInterval. The thing is, it looks like it doesn't change the interval and the API calls seems too fast. The basic value is 1 minute. Here is my code:
useEffect(() => {
FetchData();
const interval = setInterval(() => {
FetchData();
}, Interval)
return() => clearInterval(interval);
},[Interval])
const handleChangeInInterval = (event) =>{
console.log("Changed interval to: ", event.target.value);
setInterval(event.target.value)
}
const FetchData = async () =>{
const resp = await axios.get("http://localhost:8080/stock/getcandle/AAPL/1")
console.log(resp);
const formattedData = formatData(resp.data.candleDataList);
console.log(formattedData);
setStockData(formattedData);
console.log("fetch called");
}

How to get rid of memory leak in react web app

I have this code in my ReactJS web application:
useEffect(() => {
const fetchInfo = async () => {
const res = await fetch(`${api}&page=${page}`);
setLoading(true);
try {
const x = await res.json();
if (page === 1) {
setItems(x);
setAutoplay(true);
} else {
setItems({
hasMore: x.hasMore,
vacancies: [...items.vacancies, ...x.vacancies],
});
}
} catch (err){
console.log(err);
}
setLoading(false);
};
fetchInfo();
}, [page]);
When this component unmounts while running asynchronous function, it throws an error in console.
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.
How can i cancel asynchronous tasks in cleanup.
I'm assuming here that setLoading is the function setting the state after your component has un-mounted, and therefore throwing this warning. If yes, then what you need is a clean-up function.
The function passed to useEffect can return a function, which will be called before the component unmounts (you can think of it as the equivalent of the old componentWillUnmount) - more details here:
https://reactjs.org/docs/hooks-effect.html#example-using-hooks
Now what you probably want is some sort of flag to check whether it is safe for you to call setLoading, i.e, set that flag to be true by default, then set it to false in the return function. Here's a good article that should help:
https://juliangaramendy.dev/use-promise-subscription/
Now I haven't tested this but essentially your code would look something like this:
useEffect(() => {
const fetchInfo = async () => {
let isSubscribed = true;
const res = await fetch(`${api}&page=${page}`);
if (isSubscribed) setLoading(true);
try {
const x = await res.json();
if (page === 1) {
setItems(x);
setAutoplay(true);
} else {
setItems({
hasMore: x.hasMore,
vacancies: [...items.vacancies, ...x.vacancies]
});
}
} catch (err) {
console.log(err);
}
if (isSubscribed) setLoading(false);
return () => (isSubscribed = false);
};
fetchInfo();
}, [page]);

How delay redux saga action to store data update?

Can't delay action firing (initialize from redux-form) to store data update after fetching.
Store is initializing with empty account object.
At initial render getAccount action firing and triggering update of store.
useEffect see store updating and triggering getAccount action second time
second data request
END
const {getAccount, initialize} = props
prepareData = () => {...prepared obj}
useEffect(() => {
const begin = async () => {
await getAccount();
await initialize(prepareData());
};
begin();
}, [account.id]);
main aim to avoid unnecessary second request
What if you put a conditional on the begin call?
const {getAccount, initialize} = props
const {begun, setBegun} = useState(false)
prepareData = () => {...prepared obj}
useEffect(() => {
const begin = async () => {
await setBegun(true);
await getAccount();
await initialize(prepareData());
};
begun || begin();
}, [account.id]);
Should getAccount be called if account.id doesn't exist? If not then simply check that account.id exists before calling begin.
const {getAccount, initialize} = props
prepareData = () => {...prepared obj}
useEffect(() => {
// add a guard condition to exit early if account.id doesn't exist yet
if (!account.id) {
return;
}
const begin = async () => {
await getAccount();
await initialize(prepareData());
};
begin();
}, [account.id]);

Categories