What is the correct approach to cancel async requests within a React functional ponent?
I have a script that requests data from an API on load (or under certain user actions), but if this is in the process of being executed & the user navigates away, it results in the following warning:
Warning: Can't perform a React state update on an unmounted ponent. 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.
Most of what I have read solves this with the AbortController
within the ponentDidUnmount
method of a class-based ponent. Whereas, I have a functional ponent in my React app which uses Axois to make an asynchronous request to an API for data.
The function resides within a useEffect
hook in the functional ponent to ensure that the function is run when the ponent renders:
useEffect(() => {
loadFields();
}, [loadFields]);
This is the function it calls:
const loadFields = useCallback(async () => {
setIsLoading(true);
try {
await fetchFields(
fieldsDispatch,
user.client.id,
user.token,
user.client.directory
);
setVisibility(settingsDispatch, user.client.id, user.settings);
setIsLoading(false);
} catch (error) {
setIsLoading(false);
}
}, [
fieldsDispatch,
user.client.id,
user.token,
user.client.directory,
settingsDispatch,
user.settings,
]);
And this is the axios request that is triggered:
async function fetchFields(dispatch, clientId, token, folder) {
try {
const response = await api.get(clientId + "/fields", {
headers: { Authorization: "Bearer " + token },
});
// do something with the response
} catch (e) {
handleRequestError(e, "Failed fetching fields: ");
}
}
Note: the api
variable is a reference to an axios.create
object.
What is the correct approach to cancel async requests within a React functional ponent?
I have a script that requests data from an API on load (or under certain user actions), but if this is in the process of being executed & the user navigates away, it results in the following warning:
Warning: Can't perform a React state update on an unmounted ponent. 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.
Most of what I have read solves this with the AbortController
within the ponentDidUnmount
method of a class-based ponent. Whereas, I have a functional ponent in my React app which uses Axois to make an asynchronous request to an API for data.
The function resides within a useEffect
hook in the functional ponent to ensure that the function is run when the ponent renders:
useEffect(() => {
loadFields();
}, [loadFields]);
This is the function it calls:
const loadFields = useCallback(async () => {
setIsLoading(true);
try {
await fetchFields(
fieldsDispatch,
user.client.id,
user.token,
user.client.directory
);
setVisibility(settingsDispatch, user.client.id, user.settings);
setIsLoading(false);
} catch (error) {
setIsLoading(false);
}
}, [
fieldsDispatch,
user.client.id,
user.token,
user.client.directory,
settingsDispatch,
user.settings,
]);
And this is the axios request that is triggered:
async function fetchFields(dispatch, clientId, token, folder) {
try {
const response = await api.get(clientId + "/fields", {
headers: { Authorization: "Bearer " + token },
});
// do something with the response
} catch (e) {
handleRequestError(e, "Failed fetching fields: ");
}
}
Note: the api
variable is a reference to an axios.create
object.
3 Answers
Reset to default 12To Cancel a fetch
operation with axios
:
- Cancel the request with the given source token
- Ensure, you don't change ponent state, after it has been unmounted
Ad 1.)
axios
brings its own cancel API:
const source = axios.CancelToken.source();
axios.get('/user/12345', { cancelToken: source.token })
source.cancel(); // invoke to cancel request
You can use it to optimize performance by stopping an async request, that is not needed anymore. With native browser fetch
API, AbortController
would be used instead.
Ad 2.)
This will stop the warning "Warning: Can't perform a React state update on an unmounted ponent."
. E.g. you cannot call setState
on an already unmounted ponent. Here is an example Hook enforcing and encapsulating mentioned constraint.
Example: useAxiosFetch
We can incorporate both steps in a custom Hook:
function useAxiosFetch(url, { onFetched, onError, onCanceled }) {
React.useEffect(() => {
const source = axios.CancelToken.source();
let isMounted = true;
axios
.get(url, { cancelToken: source.token })
.then(res => { if (isMounted) onFetched(res); })
.catch(err => {
if (!isMounted) return; // p already unmounted, nothing to do
if (axios.isCancel(err)) onCanceled(err);
else onError(err);
});
return () => {
isMounted = false;
source.cancel();
};
}, [url, onFetched, onError, onCanceled]);
}
import React from "react";
import axios from "axios";
export default function App() {
const [mounted, setMounted] = React.useState(true);
return (
<div>
{mounted && <Comp />}
<button onClick={() => setMounted(p => !p)}>
{mounted ? "Unmount" : "Mount"}
</button>
</div>
);
}
const Comp = () => {
const [state, setState] = React.useState("Loading...");
const url = `https://jsonplaceholder.typicode./users/1?_delay=3000×tamp=${new Date().getTime()}`;
const handlers = React.useMemo(
() => ({
onFetched: res => setState(`Fetched user: ${res.data.name}`),
onCanceled: err => setState("Request canceled"),
onError: err => setState("Other error:", err.message)
}),
[]
);
const cancel = useAxiosFetch(url, handlers);
return (
<div>
<p>{state}</p>
{state === "Loading..." && (
<button onClick={cancel}>Cancel request</button>
)}
</div>
);
};
// you can extend this hook with custom config arg for futher axios options
function useAxiosFetch(url, { onFetched, onError, onCanceled }) {
const cancelRef = React.useRef();
const cancel = () => cancelRef.current && cancelRef.current.cancel();
React.useEffect(() => {
cancelRef.current = axios.CancelToken.source();
let isMounted = true;
axios
.get(url, { cancelToken: cancelRef.current.token })
.then(res => {
if (isMounted) onFetched(res);
})
.catch(err => {
if (!isMounted) return; // p already unmounted, nothing to do
if (axios.isCancel(err)) onCanceled(err);
else onError(err);
});
return () => {
isMounted = false;
cancel();
};
}, [url, onFetched, onError, onCanceled]);
return cancel;
}
useEffect has a return option which you can use. It behaves (almost) the same as the ponentDidUnmount.
useEffect(() => {
// Your axios call
return () => {
// Your abortController
}
}, []);
You can use lodash.debounce and try steps below
Stap 1:
inside constructor:
this.state{
cancelToken: axios.CancelToken,
cancel: undefined,
}
this.doDebouncedTableScroll = debounce(this.onScroll, 100);
Step 2: inside function that use axios add:
if (this.state.cancel !== undefined) {
cancel();
}
Step 3:
onScroll = ()=>{
axiosInstance()
.post(`xxxxxxx`)
, {data}, {
cancelToken: new cancelToken(function executor(c) {
this.setState({ cancel: c });
})
})
.then((response) => {
}