Query Cancellation alone doesn't do much per default: It cancels the Query itself (so data is not stored), but it doesn't abort the request.
To achieve that, you need to forward the AbortSignal provided by react-query to your fetch function. How you do this depends on what you use for data fetching - the docs have multiple examples, but here is the one for fetch:
const query = useQuery({
queryKey: ['todos'],
queryFn: async ({ signal }) => {
const todosResponse = await fetch('/todos', { signal })
return todoResponse.json()
}
})
If the signal is passed to fetch, the Query will be cancelled automatically when the fetch is aborted. This will happen as soon as no component is mounted that is actively using the Query.
Query Cancellation alone doesn't do much per default: It cancels the Query itself (so data is not stored), but it doesn't abort the request.
To achieve that, you need to forward the AbortSignal provided by react-query to your fetch function. How you do this depends on what you use for data fetching - the docs have multiple examples, but here is the one for fetch:
const query = useQuery({
queryKey: ['todos'],
queryFn: async ({ signal }) => {
const todosResponse = await fetch('/todos', { signal })
return todoResponse.json()
}
})
If the signal is passed to fetch, the Query will be cancelled automatically when the fetch is aborted. This will happen as soon as no component is mounted that is actively using the Query.
You are almost there and missing a curly braces {} in your if statement.The code should be this :
const queryClient = useQueryClient();
useEffect(() => {
return () => {
queryClient.getQueryCache().getAll().forEach(query => {
if (query.queryKey.includes('partOfMyKey')) {
queryClient.cancelQueries(query.queryKey);
}
});
};
}, [window.location.href]);
A call to fetchQuery is cancelled when React useQuery is unmounted
How important is it to cancel network requests when a component unmounts?
reactjs - How to cancel a fetch on componentWillUnmount - Stack Overflow
docs: useQuery abortOnUnmount has more effects than the name and documentation suggest
Videos
If a fetch request keeps running after a component has unmounted, we are wasting resources because we don't have a use for the return value of that request anymore (since the component showing the data was removed).
I'm trying to understand if request cancellation is considered merely an "optimization" or absolutely important. Assuming that the request doesn't do anything particularly expensive, just a database request on the backend.
Also, what about POST and PATCH requests? Don't we wanna finish these even if we navigated away?
And what about libraries like React-query and SWR that cache the response? Does the caching make it okay to load an unused response, considering that we can reuse it later to show cached data?
When you fire a Promise it might take a few seconds before it resolves and by that time user might have navigated to another place in your app. So when Promise resolves setState is executed on unmounted component and you get an error - just like in your case. This may also cause memory leaks.
That's why it is best to move some of your asynchronous logic out of components.
Otherwise, you will need to somehow cancel your Promise. Alternatively - as a last resort technique (it's an antipattern) - you can keep a variable to check whether the component is still mounted:
componentDidMount(){
this.mounted = true;
this.props.fetchData().then((response) => {
if(this.mounted) {
this.setState({ data: response })
}
})
}
componentWillUnmount(){
this.mounted = false;
}
I will stress that again - this is an antipattern but may be sufficient in your case (just like they did with Formik implementation).
A similar discussion on GitHub
EDIT:
This is probably how would I solve the same problem (having nothing but React) with Hooks:
OPTION A:
import React, { useState, useEffect } from "react";
export default function Page() {
const value = usePromise("https://something.com/api/");
return (
<p>{value ? value : "fetching data..."}</p>
);
}
function usePromise(url) {
const [value, setState] = useState(null);
useEffect(() => {
let isMounted = true; // track whether component is mounted
request.get(url)
.then(result => {
if (isMounted) {
setState(result);
}
});
return () => {
// clean up
isMounted = false;
};
}, []); // only on "didMount"
return value;
}
OPTION B: Alternatively with useRef which behaves like a static property of a class which means it doesn't make component rerender when it's value changes:
function usePromise2(url) {
const isMounted = React.useRef(true)
const [value, setState] = useState(null);
useEffect(() => {
return () => {
isMounted.current = false;
};
}, []);
useEffect(() => {
request.get(url)
.then(result => {
if (isMounted.current) {
setState(result);
}
});
}, []);
return value;
}
// or extract it to custom hook:
function useIsMounted() {
const isMounted = React.useRef(true)
useEffect(() => {
return () => {
isMounted.current = false;
};
}, []);
return isMounted; // returning "isMounted.current" wouldn't work because we would return unmutable primitive
}
Example: https://codesandbox.io/s/86n1wq2z8
The friendly people at React recommend wrapping your fetch calls/promises in a cancelable promise. While there is no recommendation in that documentation to keep the code separate from the class or function with the fetch, this seems advisable because other classes and functions are likely to need this functionality, code duplication is an anti-pattern, and regardless the lingering code should be disposed of or canceled in componentWillUnmount(). As per React, you can call cancel() on the wrapped promise in componentWillUnmount to avoid setting state on an unmounted component.
The provided code would look something like these code snippets if we use React as a guide:
const makeCancelable = (promise) => {
let hasCanceled_ = false;
const wrappedPromise = new Promise((resolve, reject) => {
promise.then(
val => hasCanceled_ ? reject({isCanceled: true}) : resolve(val),
error => hasCanceled_ ? reject({isCanceled: true}) : reject(error)
);
});
return {
promise: wrappedPromise,
cancel() {
hasCanceled_ = true;
},
};
};
const cancelablePromise = makeCancelable(fetch('LINK HERE'));
constructor(props){
super(props);
this.state = {
isLoading: true,
dataSource: [{
name: 'loading...',
id: 'loading',
}]
}
}
componentDidMount(){
cancelablePromise.
.then((response) => response.json())
.then((responseJson) => {
this.setState({
isLoading: false,
dataSource: responseJson,
}, () => {
});
})
.catch((error) =>{
console.error(error);
});
}
componentWillUnmount() {
cancelablePromise.cancel();
}
---- EDIT ----
I have found the given answer may not be quite correct by following the issue on GitHub. Here is one version that I use which works for my purposes:
export const makeCancelableFunction = (fn) => {
let hasCanceled = false;
return {
promise: (val) => new Promise((resolve, reject) => {
if (hasCanceled) {
fn = null;
} else {
fn(val);
resolve(val);
}
}),
cancel() {
hasCanceled = true;
}
};
};
The idea was to help the garbage collector free up memory by making the function or whatever you use null.
Let's say I have a request which takes enough time for a user to navigate away before completion (maybe they change there mind, etc), I want this request to be cancelled to prevent unwanted behaviour. Does React automatically handle this? Or do I need to somehow cancel the fetch request?
In my case I have a modal with user authentication form, and am trying to figure out how to cancel the request if the user closes the modal before the request finishes, although this is not an issue with this modal as the response time is <1s I could see this occurring at some point in the future with other request types. I don't want the user to close the modal and the request to still complete as that does not make sense.
Any ideas would be great, I feel I am probably overthinking this.
You cannot attach .cancel to a promise in an async functions because async functions will always return a new Promise without the cancel function you attached. If you want to attach a cancel functions, it's best to use promise chaining instead of async functions.
Even better, react-query supports cancellation now by providing an AbortSignal out of the box, which you only need to attach to your request.
with axios 0.22+ and react-query v3.30+, you can do:
const query = useQuery('key', ({ signal }) =>
axios.get('/xxxx', {
signal,
})
)
with that, react-query will cancel the network request of your query if the query becomes unused or if you call queryClient.cancelQueries('key'), e.g. when the user clicks a button.
You need to throw out an error based on the response in your getData(). React-Query will catch that error and trigger the onError event.