React : Access multiple states in setInterval() in useEffect() - reactjs

I have this code which updates the state count every 1 seconds.
How can I access the value of the state object in setInterval() ?
import React, {useState, useEffect, useCallback} from 'react';
import axios from 'axios';
export default function Timer({objectId}) {
const [object, setObject] = useState({increment: 1});
const [count, setCount] = useState(0);
useEffect(() => {
callAPI(); // update state.object.increment
const timer = setInterval(() => {
setCount(count => count + object.increment); // update state.count with state.object.increment
}, 1000);
return () => clearTimeout(timer); // Help to eliminate the potential of stacking timeouts and causing an error
}, [objectId]); // ensure this calls only once the API
const callAPI = async () => {
return await axios
.get(`/get-object/${objectId}`)
.then(response => {
setObject(response.data);
})
};
return (
<div>{count}</div>
)
}
The only solution I found is this :
// Only this seems to work
const timer = setInterval(() => {
let increment = null;
setObject(object => { increment=object.increment; return object;}); // huge hack to get the value of the 2nd state
setCount(count => count + increment);
}, 1000);

In your interval you have closures on object.increment, you should use useRef instead:
const objectRef = useRef({ increment: 1 });
useEffect(() => {
const callAPI = async () => {
return await axios.get(`/get-object/${objectId}`).then((response) => {
objectRef.current.increment = response.data;
});
};
callAPI();
const timer = setInterval(() => {
setCount((count) => count + objectRef.current);
}, 1000);
return () => {
clearTimeout(timer);
};
}, [objectId]);

Related

React UseEffect hook Issue Rendering an Interval

In the following React Component below, I am trying to add increment count by each second passed so it looks like a stopwatch, but the count is shown as 2, then blinks to 3, and back to 2. Does anyone know how to deal with this bug, and get the count to show up as intended?
import React, { useEffect, useState } from "react";
const IntervalHook = () => {
const [count, setCount] = useState(0);
const tick = () => {
setCount(count + 1);
};
useEffect(() => {
const interval = setInterval(tick, 1000);
return () => {
clearInterval(interval);
};
}, [ count ]);
return <h1> {count} </h1>;
};
export default IntervalHook;
if you want to change some state based on its previous value, use a function:setCount(count => count + 1); and your useEffect becomes independant of [ count ]. Like
useEffect(() => {
const tick = () => {
setCount(count => count + 1);
};
const interval = setInterval(tick, 1000);
return () => {
clearInterval(interval);
};
}, [setCount]);
or you get rid of tick() and write.
useEffect(() => {
const interval = setInterval(setCount, 1000, count => count + 1);
return () => {
clearInterval(interval);
};
}, [setCount]);
But imo it's cleaner to use a reducer:
const [count, increment] = useReducer(count => count + 1, 0);
useEffect(() => {
const interval = setInterval(increment, 1000);
return () => {
clearInterval(interval);
};
}, [increment]);

Setting state in useEffect hook does not change state value

am writing an app in react native and i got a problem with useState and useEffect hooks. I would like to change increment state value by one every 10 seconds.
Here is my code:
const [tripDistance, setTripDistance] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setTripDistance((prevState) => prevState + 1);
console.log(tripDistance);
}, 10000);
return () => {
clearInterval(interval);
};
}, []);
but the output from the console.log is always 0.
What am I doing wrong?
The output is always zero because in your useEffect you are not listening for the changes on tripDistance state. When you call setTripDistance you cannot access the updated value immediately.
You should add another useEffect that listen on tripDistance in order to have the correct console.log.
So you have to do something like:
const [tripDistance, setTripDistance] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setTripDistance((prevState) => prevState + 1);
}, 10000);
return () => {
clearInterval(interval);
};
}, []);
useEffect(() => console.log(tripDistance), [tripDistance]);
try this
const [tripDistance, setTripDistance] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setTripDistance((prevState) => prevState + 1);
}, 10000);
return () => {
clearInterval(interval);
};
}, [tripDistance]);

Can't perform a React state update on an unmounted component. This is a no-op

That's the warning in the 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.
Here is my code
const [index, setIndex] = useState(0);
const [refreshing, setRefreshing] = useState(false);
const refContainer: any = useRef();
const [selectedIndex, setSelectedIndex] = useState(0);
const navigation = useNavigation();
useEffect(() => {
refContainer.current.scrollToIndex({animated: true, index});
}, [index]);
const theNext = (index: number) => {
if (index < departments.length - 1) {
setIndex(index + 1);
setSelectedIndex(index + 1);
}
};
setTimeout(() => {
theNext(index);
if (index === departments.length - 1) {
setIndex(0);
setSelectedIndex(0);
}
}, 4000);
const onRefresh = () => {
if (refreshing === false) {
setRefreshing(true);
setTimeout(() => {
setRefreshing(false);
}, 2000);
}
};
What should I do to make clean up?
I tried to do many things but the warning doesn't disappear
setTimeout need to use in useEffect instead. And add clear timeout in return
useEffect(() => {
const timeOut = setTimeout(() => {
theNext(index);
if (index === departments.length - 1) {
setIndex(0);
setSelectedIndex(0);
}
}, 4000);
return () => {
if (timeOut) {
clearTimeout(timeOut);
}
};
}, []);
Here is a simple solution. first of all, you have to remove all the timers like this.
useEffect(() => {
return () => remover timers here ;
},[])
and put this
import React, { useEffect,useRef, useState } from 'react'
const Example = () => {
const isScreenMounted = useRef(true)
useEffect(() => {
isScreenMounted.current = true
return () => isScreenMounted.current = false
},[])
const somefunction = () => {
// put this statement before every state update and you will never get that earrning
if(!isScreenMounted.current) return;
/// put here state update function
}
return null
}
export default Example;

React hooks useInterval reset after button click

I have hook useInterval which download data every 10 seconds automaticaly, however I have also button which can manually download data in every moment. I'm struggling to restart interval timer when I click button. So basically if interval counts to 5, but I click button meantime, interval should restart and starts counting to 10 again before downloading data
const useInterval = (callback, delay) => {
const savedCallback = useRef(callback);
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
const tick = () => {
savedCallback.current();
}
if (delay !== null) {
const id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
};
export default useInterval;
APP PART:
useInterval(() => {
getMessage();
}, 10000)
const getMessage = async () => {
setProcessing(true)
try {
const res = await fetch('url')
const response = await res.json();
setRecievedData(response)
}
catch (e) {
console.log(e)
}
finally {
setProcessing(false)
}
}
const getMessageManually = () => {
getMessage()
RESTART INTERVAL
}
You can add a reset function in the hook and return that function. The reset function should clear the existing interval and start a new one.
Here is the code for the hook which can be reset and stopped.
const useInterval = (callback, delay) => {
const savedCallback = useRef(callback);
const intervalRef = useRef(null);
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
if (delay !== null) {
const id = setInterval(savedCallback.current, delay);
intervalRef.current = id;
return () => clearInterval(id);
}
}, [delay]);
useEffect(()=>{
// clear interval on when component gets removed to avoid memory leaks
return () => clearInterval(intervalRef.current);
},[])
const reset = useCallback(() => {
if(intervalRef.current!==null){
clearInterval(intervalRef.current);
intervalRef.current = setInterval(savedCallback.current,delay)
}
});
const stop = useCallback(() => {
if(intervalRef.current!==null){
clearInterval(intervalRef.current);
}
})
return {
reset,
stop
};
};
// usage
const {reset,stop} = useInterval(()=>{},10000);
reset();
stop();
You should add a reset function as returning a value from the hook.
I also fixed few issues and added an unmount handler:
// Usage
const resetInterval = useInterval(() => ..., DELAY);
resetInterval();
// Implementation
const useInterval = (callback, delay) => {
const savedCallbackRef = useRef(callback);
const intervalIdRef = useRef();
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// handle tick
useEffect(() => {
const tick = () => {
savedCallback.current();
};
if (delay !== null) {
intervalIdRef.current = setInterval(tick, delay);
}
const id = intervalIdRef.current;
return () => {
clearInterval(id);
};
}, [delay]);
// handle unmount
useEffect(() => {
const id = intervalIdRef.current;
return () => {
clearInterval(id);
};
}, []);
const resetInterval = useCallback(() => {
clearInterval(intervalIdRef.current);
intervalIdRef.current = setInterval(savedCallback.current, delay)
}, [delay]);
return resetInterval;
};
Another solution is to remove the ref on the callback making the hook restart the count on every change to the callback
so updating the above solution
// Implementation
const useInterval = (callback, delay) => {
const intervalIdRef = useRef();
// handle tick
useEffect(() => {
const tick = () => {
callback();
};
if (delay !== null) {
intervalIdRef.current = setInterval(tick, delay);
}
const id = intervalIdRef.current;
return () => {
clearInterval(id);
};
}, [delay]);
// handle unmount
useEffect(() => {
const id = intervalIdRef.current;
return () => {
clearInterval(id);
};
}, []);
};
And then you can use it like this
const [counter, setCounter] = useState[0]
const onTimerFinish = useCallback(() => {
setCounter(counter + 1)
// setCounter will reset the interval
}, [counter])
useResetInterval(() => {
onTimerFinish()
}, 5000)

react useState don't update properly when it's called inside callback

I am going to count loaded apis. I used function component.
function Screen(props) {
const [count, setCount] = useState(0)
useEffect(() => {
loadData();
() => { return null; }
}, [])
const loadData = () => {
axios({ url: API_BASE_URL + 'countries', method: 'get' }).then((res)=>{
setCount(count+1)
})
axios({ url: API_BASE_URL + 'regions', method: 'get' }).then((res)=>{
setCount(count+1)
})
}
useEffect(() => {
console.info(count) // this shows 0, 1, it never be 2
}, [count])
return <div></div>
}
what's wrong with my code? I think it should print 2 at last
Thanks
count is captured as 0 by the closures you pass to then. Effectively you're calling setCount(0 + 1). A very thorough explanation can be found here: https://overreacted.io/a-complete-guide-to-useeffect/
Here's an updated version of your code, that fixes the issues:
import React, { useCallback, useEffect, useState } from 'react';
const fakeApi = (delay = 400, val = null): Promise<typeof val> => new Promise(resolve => {
setTimeout(resolve, delay, val);
});
const Count: React.FC = () => {
const [count, setCount] = useState(0);
const loadData = useCallback(() => {
fakeApi(1000).then(() => {
setCount(count => count + 1); // use the function form of setState to get the current state value without being *dependent* on it.
});
fakeApi(2000).then(() => {
setCount(count => count + 1); // same as above.
});
}, []); // no dependencies => loadData is constant
useEffect(() => {
loadData();
}, [loadData]); // dependent on loadData, but that changes only once (first render, see above).
// note: you could move loadData inside the effect to reduce some code noise. Example:
//
// useEffect(() => {
// const loadData = () => {
// fakeApi(1000).then(() => {
// setCount(count => count + 1);
// });
//
// fakeApi(2000).then(() => {
// setCount(count => count + 1);
// });
// };
//
// loadData();
// }, []);
return (
<div>{count}</div>
);
};
export default Count;
Demo: https://codesandbox.io/s/focused-sun-0cz6u?file=/src/App.tsx

Resources