Issue
I'm quite new to the React-TS world and I have recently been playing with useState
and useEffect
hooks only basically.
I have the following functional component inside which I'd like to fetch N items the first time and then start a periodic function that fetches the last item from the response data, updating the current state.
const fetcher = async (url: string) => await axios.get(url).then((res: AxiosResponse) => res.data);
type AirflowData = {
value: number; // perc values from 0 to 1
timestamp: number; // UTC time
};
const ActionDetector: React.FC = () => {
const [alerts, setAlerts] = useState<AirflowData[]>([]);
useEffect(() => {
// Fetch the latest N alerts first
getAlerts(100);
// Then start fetching the last alert every N milliseconds
const interval = setInterval(() => getLatestAlert(), 1000);
// Clear interval
return () => {
clearInterval(interval);
};
}, []);
/**
* Return the alert data after fetching it.
* @param numAlerts number of the last N alerts to return
*/
const getAlerts = async (numAlerts: number) => {
const fetchedAlerts: AirflowData[] = await fetcher("http://localhost:9500/alerts");
setAlerts(fetchedAlerts.slice(-numAlerts));
};
/**
* Return the latest alert data available.
*/
const getLatestAlert = async () => {
const fetchedAlerts: AirflowData[] = await fetcher("http://localhost:9500/alerts");
const latestFetchedAlert = fetchedAlerts.slice(-1)[0];
const latestAlert = alerts.slice(-1)[0];
if (latestFetchedAlert && latestAlert && latestFetchedAlert.timestamp !== latestAlert.timestamp) {
// Append the alert only if different from the previous one
setAlerts([...alerts, latestFetchedAlert]);
}
};
console.log(alerts);
return <></>
}
export default ActionDetector
The problem with this approach is that latestAlert
is always undefined
and that is due, if I understood how React works under the hood correctly, to the initial state change re-rendering trigger. After getAlerts()
is called and fires setAlerts(...)
, the component starts the re-rendering and so, since getLatestAlert()
is called inside the useEffect
only the first time (the first render), it always read alerts
as the initialized empty array.
I don't know if this is the correct reason behind this, but how can I achieve what I'm trying to do the right way?
Solution
The fundamental issue is that when updating state based on existing state, you need to be sure you have the latest state information. Your getLatestAlerts
function closes over the alerts
constant that was in scope when it was created, so it only ever uses that version of the constant (not the updated one from a subsequent render). Your useEffect
setInterval
callback closes over the getLatestAlerts
function that was in scope when it was created, and only ever uses that version of the function.
To be sure you have the latest state, use the callback version of the state setter instead of the constant:
const getLatestAlert = async () => {
const fetchedAlerts: AirflowData[] = await fetcher("http://localhost:9500/alerts");
const latestFetchedAlert = fetchedAlerts.slice(-1)[0];
if (latestFetchedAlert) {
setAlerts(alerts => {
const latestAlert = alerts.slice(-1)[0];
if (latestFetchedAlert && latestAlert && latestFetchedAlert.timestamp !== latestAlert.timestamp) {
// Append the alert only if different from the previous one
alerts = [...alerts, latestFetchedAlert];
}
return alerts;
});
}
};
Purely as a side note, I wouldn't use the idiom you seem to be using to get the last item from an array, array.slice(-1)[0]
. Instead, I'd either use array[array.length - 1]
, or use the at
method which just achieved Stage 4 and will be in this year's spec (it's easily polyfilled for older environments).
Answered By - T.J. Crowder
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.