Cancel async fetch request in React Native using Expo - javascript

I have a React Native app that has been built using Expo (v35.0.0). I have a simple async function (loadData()) that runs a fetch (via fetchData()) request to an API that the response is then passed into my redux store:
const loadData = async (id, token) => {
setIsLoading(true);
try {
await dispatch(fetchData(id, token));
} catch (error) {
setHasError(true);
}
setIsLoading(false);
};
useEffect(() => {
loadData(user.client.id, user.token);
}, [user.client.id]);
However, when the user logs out we are presented with the warning: "Can't perform a React state update on an unmounted component", which I understand is because the async request has been cancelled.
I have attempted to implement the AbortController approach (as outlined in this article: https://dev.to/iquirino/react-hook-clean-up-useeffect-24e7), however then we are presented with an error stating AbortConroller is unknown.
I thought that support for AbortConroller would be within Expo now as it was added to React Native back in July last year as part of the v0.60 release.
So, is it possible to implement AbortController within Expo to cancel the async request? If not, how should I go about cancelling the request to avoid the warning (& memory leak)?

Because your dispatch is async, it's possible for it to finish after your component is unmounted.
Inside useEffect you can return a function that you call to cleanup. Here you can set a flag to indicate the component is no longer mounted, and if this flag indicates it's no longer mounted, you can tell it not to update state.
eg.
let mounted = true;
const loadData = async (id, token) => {
setIsLoading(true);
try {
await dispatch(fetchData(id, token));
} catch (error) {
if (mounted) setHasError(true);
}
if (mounted) setIsLoading(false);
};
useEffect(() => {
loadData(user.client.id, user.token);
return () => mounted = false;
}, [user.client.id]);

Related

React 18 strict mode causing component to render twice

The changes to strict-mode in React version 18 causes my code to render twice, which causes an error in axios abort controller, but I don't know how to clear the error from the browser console after the app renders twice.
Please note: I am working on a sign-up / log-in app and even after I successfully logged in, React takes me back to the log-in page, because of the axios error
useEffect(() => {
let isMounted = true;
// used by axios to cancel request
const controller = new AbortController();
const getGoals = async () => {
try {
const response = await goalPrivate.get("/goals", {
// option to cancel request
signal: controller.signal
})
console.log(response?.data);
// set goals state when component mounts
isMounted && setGoals(response?.data);
} catch (error) {
console.log(error.message);
// when refreshToken expires
navigate("/login", { state: { from: location }, replace: true });
}
}
getGoals();
// cleanup function
return () => {
// don't set state if component unmounts
isMounted = false;
// cancel request if component unmounts
controller.abort();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
React StrictMode calls all Effects twice to make sure their cleanup/unmount handlers work as intended. You may need to change your effects accordingly, even if they have an empty dependency list and would normally not unmount before the site is closed.
Note, this only happens in Strict + development mode. In a production build, effects will only be called once and when their dependencies change.
Fore more context, see https://reactjs.org/docs/strict-mode.html#ensuring-reusable-state
Had the same problem and fixed it this way.
When the abortController is aborted you jump to the catch so you just check if the signal is aborted or not to execute the rest of your code.
useEffect(() => {
const abortController = new AbortController();
fetch("https://pokeapi.co/api/v2/pokemon", {
signal: abortController.signal,
})
.then((res) => res.json())
.then(console.log)
.catch((err) => {
if (abortController.signal.aborted) return;
console.log(err);
// Your navigate
});
return () => {
abortController.abort();
};
}, []);
If you have the StrictMode enabled, it will fire two times the useEffect on development mode to make sure that you are aware of the possible side-effects that could appear.
You should classify the error response depends on error code or http status code.
Eg:
...
try {
// Create axios request
} catch (e: AxiosError) {
if (error.code === 'ERR_CANCELED') {
// When abort controller executed
} else (error.response.status == 401) {
// When you get http code 401 (Un-authenticated)
// Eg:
navigate("/login", { state: { from: location }, replace: true });
} else {
// Etc...
}
}
...
React 18 now has Strict.Mode mount, unmount, and remount components which causes the abortController to issue an error on the first unmount. Remember, this only happens in development mode when Strict.Mode is applied in your index.js. We can check for that behaviour while in development-mode.
try {
// fetch API data
} catch (error) {
if (process.env.NODE_ENV === "development" && error) {
// ignore the error
console.log(error.message);
} else {
// when refreshToken expires, go back to login
navigate("/login", { state: { from: location }, replace: true });
}
}

React SetState within a async function

I'm trying to learn React making a weather application and I have got stacked using the async/await function. Here is the trouble I'm facing..
I have a function which does a axios call to an api and then I'm trying to set some state variables. The think is that it seems the application is running the setState methods before the promise is resolved.
Here is the method that makes the axios call.
const fetchWeeklyWeather = async () => {
try {
let response = await axios.get(url);
console.log('%cFetch Weekly Weather Response:', 'color: #bada55', response);
setCurrentWeather(response.data.current);
setWeeklyWeather(response.data.daily);
setWeatherAlerts(response.data.alerts);
} catch (err) {
// Handle Error Here
console.error(err);
}
}
And I call that method on the componentDidMount like so:
useEffect(() => {
fetchWeeklyWeather();
console.log("Current Weather", currentWeather);
console.log("Weekly Weather", weeklyWeather);
console.log("Weather alerts", weatherAlerts);
formatData(weeklyWeather);
}, []);
It is weird because sometimes it works but most of the time it does not. I guess I'm not doing the right think with the async/await. Can anyone give me a hand?
Thank you so much!
Lots of discussion here React setState not Updating Immediately
But basically, setState is async/ a request to update state in the next render. Therefor, the updates to state are not immediately available/observable
useEffect(() => {
fetchWeeklyWeather();
console.log("Current Weather", currentWeather);
console.log("Weekly Weather", weeklyWeather);
console.log("Weather alerts", weatherAlerts);
formatData(weeklyWeather);
}, []);
The callback is only executed on component mount (empty dependancy array), so the console.log() statements will only log the initial values of currentWeather, weeklyWeather and weatherAlerts, not the values that are set retrieved by axios.
To fix this you should separate the useEffect callback into multiple callbacks.
// runs on component mount
useEffect(() => {
fetchWeeklyWeather();
}, []);
// runs on component mount and whenever currentWeather changes
useEffect(() => {
console.log("Current Weather", currentWeather);
}, [currentWeather]);
// runs on component mount and whenever weeklyWeather changes
useEffect(() => {
console.log("Weekly Weather", weeklyWeather);
formatData(weeklyWeather);
}, [weeklyWeather]);
// runs on component mount and whenever weatherAlerts changes
useEffect(() => {
console.log("Weather alerts", weatherAlerts);
}, [weatherAlerts]);
The above should log currentWeather twice. Once with the initial value, and once with the value that is loaded by axios. The same applies for weeklyWeather and weatherAlerts.
Note that formatData() is also called twice. So it must be able to handle the call with the initial data. If this initial data is null you might want to skip the initial call.
const [weeklyWeather, setWeeklyWeather] = useState(null);
// initial weeklyWeather value ^^^^
// ...
// runs on component mount and whenever weeklyWeather changes
useEffect(() => {
console.log("Weekly Weather", weeklyWeather);
// skip the formatData() call if weeklyWeather is null
if (weeklyWeather) formatData(weeklyWeather);
}, [weeklyWeather]);
The above will skip the formatData() call when weeklyWeather is null (or any other falsy value). Meaning that it will not be called for the initial value, but it is called once the axios response updated the state.

React : Updating and then accessing state after a network request in useEffect hook. State remains stale

Im trying to update and reference hasError state field inside of the initialization function of my component in order to control if a redirect happens after successful initialization or if error gets displayed.
Here is a condensed version of the issue:
const [hasError, setHasError] = useState(false);
useEffect(() => {
initialize();
}, []);
async function initialize(){
try {
await networkRequest();
} catch (err) {
setHasError(true);
}
console.log(hasError); // <- is still false
if(!hasError){
redirect() // <- causes redirect even with error
}
}
function networkRequest() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject();
}, 1000);
});
}
The initialization function should only be called once on component mount which is why I'm passing [] to useEffect. Passing [hasError] to useEffect also doesn't make sense since I don't want initialization to run everytime hasError updates.
I have seen people recommend using useReducer but that seems hacky since I'm already using Redux on this component and I'll need to use 2 different dispatch instances then.
How is a use case like this typically handled?
You will have to create another useEffect hook that "listens" for changes to hasError. setHasError in this case is asynchronous, so the new value won't be available to you immediately.
I don't know what the rest of your component looks like, but it sounds like you should have some sort of isLoading state that will be used to display a loading message, then once your request finishes and it fails, you render what you need, or if it succeeds, you redirect.
Here's an example:
function App() {
const [isLoading, setIsLoading] = useState(true);
const [hasError, setHasError] = useState(false);
useEffect(() => {
(async () => {
try {
await networkRequest();
isLoading(false);
} catch (error) {
setHasError(error);
isLoading(false);
}
})()
}, [])
useEffect(() => {
if (!isLoading && !hasError) {
redirect();
}
}, [isLoading, hasError]);
if (isLoading) { return "Loading"; }
// else, do whatever you need to do here
}

Can't perform a React state update on an unmounted component with useEffect hook

I have
useEffect(() => {
setLoading(true);
axios
.get(url, {params})
.then(data => {
setData(data || []);
setLoading(false);
})
.catch(() => {
showToast('Load data failed!', 'error');
setLoading(false);
});
}, [params]);
It gives me
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.
Ok, the question IS NOT HOW TO SOLVE IT. When I use setLoading(false) after axios promise it works fine but inside of promise (e.g. above) it always gives me warning. Actually I want to know WHY IT HAPPENS SO? Is there anybody who may explain me in a nutshell a flow of code above (the process how code above works with warning) and maybe give some best practices on using hooks.
you need clean up function.
this means you should call function end of useEffect function.
when dependencie is changes (params as your example ) calls that function.
so we would be able controll when component mounts/unmounts
useEffect(() => {
let cancelled = false;
setLoading(false);
async function fetchData() {
try {
const response = await axios.get(url, { params });
if (!cancelled) {
setData(response.data);
setLoading(false);
}
} catch (e) {
if (!cancelled) {
showToast(e.message, "error");
setLoading(false);
}
}
}
fetchData();
// clean up here
return () => {
cancelled = true;
};
}, [params]);
WHY IT HAPPENS SO?
Imagine your request is goes slow, and the component has already unmounted when the async request finishes. this time throws this warning

Function inside component not receiving latest version of Redux-state to quit polling

I have an issue where I am trying to use the Redux state to halt the execution of some polling by using the state in an if conditional. I have gone through posts of SO and blogs but none deal with my issue, unfortunately. I have checked that I am using mapStateToProps correctly, I update state immutably, and I am using Redux-Thunk for async actions. Some posts I have looked at are:
Component not receiving new props
React componentDidUpdate not receiving latest props
Redux store updates successfully, but component's mapStateToProps receiving old state
I was kindly helped with the polling methodology in this post:Incorporating async actions, promise.then() and recursive setTimeout whilst avoiding "deferred antipattern" but I wanted to use the redux-state as a single source of truth, but perhaps this is not possible in my use-case.
I have trimmed down the code for readability of the actual issue to only include relevant aspects as I have a large amount of code. I am happy to post it all but wanted to keep the question as lean as possible.
Loader.js
import { connect } from 'react-redux';
import { delay } from '../../shared/utility'
import * as actions from '../../store/actions/index';
const Loader = (props) => {
const pollDatabase = (jobId, pollFunction) => {
return delay(5000)
.then(pollFunction(jobId))
.catch(err => console.log("Failed in pollDatabase function. Error: ", err))
};
const pollUntilComplete = (jobId, pollFunction) => {
return pollDatabase(jobId, pollFunction)
.then(res => {
console.log(props.loadJobCompletionStatus) // <- always null
if (!props.loadJobCompletionStatus) { <-- This is always null which is the initial state in reducer
return pollUntilComplete(jobId, pollFunction);
}
})
.catch(err=>console.log("Failed in pollUntilComplete. Error: ", err));
};
const uploadHandler = () => {
...
const transferPromise = apiCall1() // Names changed to reduce code
.then(res=> {
return axios.post(api2url, res.data.id);
})
.then(postResponse=> {
return axios.put(api3url, file)
.then(()=>{
return instance.post(api3url, postResponse.data)
})
})
transferDataPromise.then((res) => {
return pollUntilComplete(res.data.job_id,
props.checkLoadTaskStatus)
})
.then(res => console.log("Task complete: ", res))
.catch(err => console.log("An error occurred: ", err))
}
return ( ...); //
const mapStateToProps = state => {
return {
datasets: state.datasets,
loadJobCompletionStatus: state.loadJobCompletionStatus,
loadJobErrorStatus: state.loadJobErrorStatus,
loadJobIsPolling: state.loadJobPollingFirestore
}
}
const mapDispatchToProps = dispatch => {
return {
checkLoadTaskStatus: (jobId) =>
dispatch(actions.loadTaskStatusInit(jobId))
};
};
export default connect(mapStateToProps, mapDispatchToProps)(DataLoader);
delay.js
export const delay = (millis) => {
return new Promise((resolve) => setTimeout(resolve, millis));
}
actions.js
...
export const loadTaskStatusInit = (jobId) => {
return dispatch => {
dispatch(loadTaskStatusStart()); //
const docRef = firestore.collection('coll').doc(jobId)
return docRef.get()
.then(jobData=>{
const completionStatus = jobData.data().complete;
const errorStatus = jobData.data().error;
dispatch(loadTaskStatusSuccess(completionStatus, errorStatus))
},
error => {
dispatch(loadTaskStatusFail(error));
})
};
}
It seems that when I console log the value of props.loadJobCompletionStatus is always null, which is the initial state of in my reducer. Using Redux-dev tools I see that the state does indeed update and all actions take place as I expected.
I initially had placed the props.loadJobCompletionStatus as an argument to pollDatabase and thought I had perhaps created a closure, and so I removed the arguments in the function definition so that the function would fetch the results from the "upper" levels of scope, hoping it would fetch the latest Redux state. I am unsure as to why I am left with a stale version of the state. This causes my if statement to always execute and thus I have infinite polling of the database.
Can anybody point out what might be causing this?
Thanks
I'm pretty sure this is because you are defining a closure in a function component, and thus the closure is capturing a reference to the existing props at the time the closure was defined. See Dan Abramov's extensive post "The Complete Guide to useEffect" to better understand how closures and function components relate to each other.
As alternatives, you could move the polling logic out of the component and execute it in a thunk (where it has access to getState()), or use the useRef() hook to have a mutable value that could be accessed over time (and potentially use a useEffect() to store the latest props value in that ref after each re-render). There are probably existing hooks available that would do something similar to that useRef() approach as well.

Categories