Apply fetch callback only if state is true - reactjs

Some data is fetched from an API if users click a button inside my React app. If another button is clicked before the API call has finished, the callback function should not be applied. Unfortunately, the state ("loading" in my code example) has not the correct value inside the callback. What am I doing wrong?
const [apartments, setApartments] = useState(emptyFeatureCollection);
const [loading, setLoading] = useState(true);
function getApartments() {
fetch(`https://any-api-endpoint.com`)
.then(response => response.json())
.then(data => {
if (loading) setApartments(data);
})
.catch(error => console.error(error));
}
}
useEffect(() => {
setLoading(false);
}, [apartments]);
function clickStartButton() {
setLoading(true);
getApartments();
}
function clickCancelButton() {
setLoading(false);
}

The issue here is that the callback code:
data => {
if (loading) setApartments(data);
}
is called in context of the original closure of getApartments() when loading was false.
That means that the callback only ever sees or "inherits" the prior loading state and, because setAppartments() relies on the updated loading state, the data from your network request is never applied.
A simple solution that requires minimal change to you code would be to pass a callback to setLoading(). Doing so would give you access to the current loading state (ie of the component rather than that of the closure in your your callback is executed). With that, you could then determine if appartment data should be updated:
function getApartments() {
/* Block fetch if currently loading */
if (loading) {
return;
}
/* Update loading state. Blocks future requests while this one
is in progress */
setLoading(true);
fetch(`https://any-api-endpoint.com`)
.then(response => response.json())
.then(data => {
/* Access the true current loading state via callback parameter */
setLoading(currLoading => {
/* currLoading is the true loading state of the component, ie
not that of the closure that getApartnment() was called */
if (currLoading) {
/* Set the apartments data seeing the component is in the
loading state */
setApartments(data);
}
/* Update the loading state to false */
return false;
});
})
.catch(error => {
console.error(error);
/* Reset loading state to false */
setLoading(false);
});
}
Here's a working example for you to see in action. Hope that helps!

Related

React setState inside useEffect async

I am attempting to perform a series of Axios requests inside the useEffect() of a react component. I am aware that these requests are asynchronous, and I should maintain a piece of "loading" state that specifies if series of requests have been completed.
const [state, updateState] = useState([])
const [loading, setLoading] = useState(true)
useEffect(() => {
let innerstate = []
allRespData.map(single_response => {
axios.post("<URL>", {
raw_narrative: single_response[index].response
})
.then((response) => {
innerstate.push(response.data)
});
})
updateState(innerstate)
setLoading(false)
}, []);
if (loading)
return (<h3> Loading </h3>)
else {
console.log(state)
return (<h3> Done </h3>)
}
I would expect the output from the above code to be a list containing the data of each response. Unfortunately, I think that data only arrives midway through the console.log() statement, as initially an empty list [] is logged, however the list is expandable- therein my expected content is visible.
I am having a hard time doing anything with my state at the top, because the list length is constantly 0, even if the response has already loaded (loading == false).
How can I assert that state has been updated? I assume the problem is that the loading variable only ensures that a call to the updateState() has been made, and does not ensure that the state has actually been updated immediately thereafter. How can I ensure that my state contains a list of response data so that I can continue doing operations on the response data, for example, state.forEach().
You're not awaiting any of the requests, so updateState will get called before any of the responses have had time to come back. You'll be setting the state as [] every time. You also need to return your axios.post or the data won't get passed to .then
There are lot of nicer ways to handle this (I'd recommend looking at the react-query library, for example). However, to make this work as it is, you could just use Promise.all(). Something like:
useEffect(() => {
Promise.all(
allRespData.map(single_response =>
axios
.post('<URL>', { raw_narrative: single_response[index].response })
.then(response => response.data)
.catch(error => {
// A single error occurred
console.error(error);
// you can throw the error here if you want Promise.all to fail (or just remove this catch)
})
)
)
// `then` will only be called when all promises are resolved
.then(responses => updateState(responses))
// add a `.catch` if you want to handle errors
.finally(() => setLoading(false));
}, []);

Call function to get data and navigate to link on data arrival

I have a situation where I should get a song item by id to get the path for that song, and then navigate to that song on button click.
Is there any specific hook that can be used to navigate on data arrival, useEffect will be called any time that state changes but the problem is that first needs to be dispatched the action to get the song, check if it returns any item and then navigate. Typically if it is has been published on the list, it should exist on the db, but the problem might be at the API side, so that check results.length > 0 is why that check is necessary.
useEffect(() => {
const handleClick = (myId: string) => {
dispatch(SongActions.searchSong(myId));
if (results.length > 0) {
if (Object.keys(results[0]).length > 0) {
// navigate(`/songs/${results[0].myPath}`);
}
}
}
}, [dispatch, results])
When user clicks on list item which has a song title, it should call the function handleClick(id) with id of the song as parameter, that is to get the metadata of the song, src path etc.
<Typography onClick={() => handleClick(songItem.songId)} sx={styles.songListItemText}>{songItem.Title}</Typography>
Edit
And this is how I have setup the searchSong action:
searchSong: (obj: SearchSongInputModel): AppThunk<SearchPayload> => async (dispatch) => {
dispatch({
payload: { isLoading: true },
type: SearchActionType.REQUEST,
});
try {
const response = await SearchApi.searchSongAsync(obj);
if (response.length === 0) {
toast.info(`No data found: ${obj.SongId}`)
}
dispatch({
type: SearchActionType.RECEIVED_SONG,
payload: { results: response },
});
} catch (e) {
console.error("Error: ", e);
}
}
You appear to be mixing up the purpose of the useEffect hook and asynchronous event handlers like button element's onClick handlers. The useEffect hook is to meant to issue intentional side-effects in response to some dependency value updating and is tied to the React component lifecycle, while onClick handlers/etc are meant to respond to asynchronous events, i.e. a user clicking a button. They don't mix.
Assuming SongActions.searchSong is an asynchronous action, you've correctly setup Redux middleware to handle them (i.e. Thunks), and the action returns the fetched response data, then the dispatched action returns a Promise that the callback can wait for.
Example:
const navigate = useNavigate();
const dispatch = useDispatch();
const handleClick = async (myId: string) => {
const results = await dispatch(SongActions.searchSong(myId));
if (results.length > 0 && Object.keys(results[0]).length > 0) {
navigate(`/songs/${results[0].myPath}`);
}
};
...
<Typography
onClick={() => handleClick(songItem.songId)}
sx={styles.songListItemText}
>
{songItem.Title}
</Typography>
The searchSong action creator should return a resolved value for consumers to await for.
searchSong: (obj: SearchSongInputModel): AppThunk<SearchPayload> => async (dispatch) => {
dispatch(startRequest());
try {
const results = await SearchApi.searchSongAsync(obj);
if (!results.length) {
toast.info(`No data found: ${obj.SongId}`)
}
dispatch(receivedSong({ results }));
return results; // <-- return resolved value here
} catch (e) {
console.error("Error: ", e);
} finally {
dispatch(completeRequest());
}
}
You can create a state such as const [isDataPresent, setIsDataPresent] = useState(false) to keep track of if the data has arrived or not. And as David has mentioned in the comments you cannot call the function inside the useEffect on handleClick. Instead what you can do is create that function outside the useEffect hook and inside the same function you fetch the data and check if the data is at all present, if present then you can set the above boolean state to true and then redirect from that function itself.
Since you are already fetching the data from the same API and different endpoint, what you can do is -
Create a new component.
Since you are mapping over the data send the data to this component by rendering it inside the map function. It'd allow the data to be passed and components to be rendered one by one.
Create a state in the new component.
Use useEffect hook to fetch the data for a single song since when you are passing the data from the previous component to this one you would also get access to the ID and store it inside the state. This would be occurring inside the newly created component.

Can't perform a React state update on an unmounted component warning in Reactjs

I'm getting this below warning in console when i run he code.
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 [userDetail, setUserDetail] = React.useState('');
const personalDetails = (user_id) => {
axios
.get(`http://localhost:3001/users/${user_id}`, { withCredentials: true })
.then((response) => {
const personalDetails = response.data;
setUserDetail(response.data);
})
.catch((error) => {
console.log(" error", error);
});
};
React.useEffect(() => {
if (user_id) {
personalDetails(user_id);
}
}, [user_id]);
If I remove useEffect call, this error disappears. What is wrong here?
This seems to be what's happening:
Your component mounts.
Your effect runs, which begins making an HTTP request.
The component unmounts for whatever reason (the parent component is removing it).
The HTTP request completes, and your callback calls setUserDetail, even though the component has been unmounted.
Another issue is that you're defining the personalDetails outside of the effect where it's used.
I would make two changes:
Move the body of the personalDetails function inside the effect.
If the component unmounts, or user_id is changed, then you don't want to call setUserDetail anymore, so use an aborted variable to keep track of this.
const [userDetail, setUserDetail] = React.useState("");
React.useEffect(() => {
if (user_id) {
let aborted = false;
axios
.get(`http://localhost:3001/users/${user_id}`, { withCredentials: true })
.then((response) => {
const personalDetails = response.data;
// Don't change state if the component has unmounted or
// if this effect is outdated
if (!aborted) {
setUserDetail(response.data);
}
})
.catch((error) => {
console.log(" error", error);
});
// The function returned here is called if the component unmounts
// or if the dependency list changes (user_id changes).
return () => (aborted = true);
}
}, [user_id]);

React Component gets unmounted and i don't know why

I'm a completely new to the whole react world but I'm trying to develop a SPA with a integrated calendar. I'm using react-router for routing, react-big-calendar for the calendar, axios for my API calls and webpack.
Whenever I'm loading my Calender Component it gets mounted and unmounted several times and I think that causes my API call to never actually get any data. I just can't figure out what is causing this.
The Code:
useEffect(() => {
console.log("mounting Calendar")
let source = Axios.CancelToken.source()
if(!initialized) {
console.log("getting Data")
getCalendarEvents(source)
}
return () => {
console.log("unmounting Calendar")
source.cancel();
}
})
const getCalendarEvents = async source => {
setInitialized(true)
setLoading(true)
try {
const response = await getCalendar({cancelToken: source.token})
const evts = response.data.map(item => {
return {
...item,
}
})
calendarStore.setCalendarEvents(evts)
} catch (error) {
if(Axios.isCancel(error)){
console.log("caught cancel")
}else{
console.log(Object.keys(error), error.message)
}
}
setLoading(false)
}
This is the result when i render the component:
Console log
If you need any more code to assess the problem, I will post it.
I appreciate any kind of input to solve my problem.
Thank you
Its because of the useEffect. If you want it to run just once on mount you need to pass an empty array as a dependency like so :
useEffect(() => {
console.log("mounting Calendar")
let source = Axios.CancelToken.source()
if(!initialized) {
console.log("getting Data")
getCalendarEvents(source)
}
return () => {
console.log("unmounting Calendar")
source.cancel();
}
},[])
This means it will only run once. If there is some state or prop you would like to keep a watch on you could pass that in the array. What this means is that useEffect will watch for changes for whatever is passed in its dependency array and rerun if it detects a change. If its empty it will just run on mount.

Unable to access properties of object stored in a React state

I am using React hooks and I need to store the data from my server which is an object, to a state.
This is my state:
const [sendingTime,setSendingTime] = useState({})
This is how I set it inside useEffect:
const getTime= () =>{
axios.get("https://localhost:1999/api/getLastUpdatedTime")
.then(res => {
console.log(res.data)
setSendingTime({sendingTime : res.data})
console.log('sendingTime is coimng',sendingTime)
})
.catch(err => console.error(err))
}
useEffect(() => {
getCount()
getTime()
},[])
Now when I console.log the object state it returns an empty object. Although if I set the object to any variable and then console.log it it doesn't return an empty object. But both ways I am unable to access the properties of the object.
Edit
This is what I was doing previously:
const[time,setTime] = useState({
totalComplaintsTime: '00:00',
resolvedComplaintsTime: '00:00',
unresolvedComplaintsTime:'00:00',
assignedComplaintsTime:'00:00',
supervisorsTime:'00:00',
rejectedComplaintsTime:'00:00'
})
const getTime= () =>{
axios.get("https://localhost:1999/api/getLastUpdatedTime")
.then(res => {
console.log(res.data)
setTime({
totalComplaintsTime: res.data.Complaints[0].updatedAt,
resolvedComplaintsTime: res.data.Resolved[0].updatedAt,
unresolvedComplaintsTime: res.data.Unresolved[0].updatedAt ,
assignedComplaintsTime: res.data.Assigned[0].updatedAt ,
rejectedComplaintsTime: res.data.Rejected[0].updatedAt,
supervisorsTime: res.data.Supervisors[0].updatedAt
})
})
.catch(err => console.error(err))
}
useEffect(() => {
getCount()
// getTime()
getTime()
},[])
And this is how I used the states to set the dynamic values :
Last updated at {time.resolvedComplaintsTime}
This is working perfectly fine but I wanted to store the data in an object state and then access it, which would've been easier and more efficient. Also I wanted to pass this object to another component. That is why I wanted to make a state and store the data in that state.
Solved
So the main problem was accessing the data throughout the component. This is the solution:
sendingTime is being initialized but only when a render occurs. So we add a piece of code to check if that state is initialized or not.
This is where I wanted to display the data.
<div key={sendingTime.length} className={classes.stats}>
<UpdateIcon fontSize={"small"} /> Last updated at{" "}
{Object.keys(sendingTime).length > 0 &&
sendingTime.Complaints[0].updatedAt}
</div>
This way I can access the properties of the object stored in the sendingTime state very easily.
setSendingTime comes from a useState so it is asynchronous:
When you call:
setSendingTime({sendingTime : res.data})
console.log('sendingTime is coimng',sendingTime)
The state sendTime has not been updated, so it display the init value which is {}
The other answers are correct, setState is asynchronous so you will only be able to get sendingTime's new value on the next re-render. And as #Enchew mentions, you probably don't want to set an object as that value most likely.
Try this instead:
const [data, setData] = useState(undefined)
const getTime = () => {
axios.get("https://localhost:1999/api/getLastUpdatedTime")
.then(({ data }) => {
console.log(data)
setData(data)
})
.catch(err => console.error(err))
}
useEffect(() => {
getCount()
getTime()
}, [])
if (!data) return <p>No data...</p>
return (
<>
<p>Complaints: {data.Complaints[0].updatedAt}<p>
<p>Resolved: {data.Resolved[0].updatedAt}<p>
<p>{/* ...etc... */}</p>
</>
)
You should see the value you're expecting to see in the console.log because you're using the locally scoped variable, not the asynchronous value, which will only be updated on the next re-render.
setSendingTime works asynchronously and your object may have not been saved in the state yet. Also pay attention to the way you save the time in your state, you are wrapping the result of the data in another object with property sendingTime, which will result in the following object: sendingTime = { sendingTime: {/*data here */} }. If you are running for sendingTime: {/*data here */} try the following:
const getTime = () => {
axios
.get('https://localhost:1999/api/getLastUpdatedTime')
.then((res) => {
console.log(res.data);
setSendingTime(res.data);
console.log('sendingTime is coimng', sendingTime);
})
.catch((err) => console.error(err));
};
Also have a look at this - how to use hooks with a callback: https://www.robinwieruch.de/react-usestate-callback

Resources