swr
swr copied to clipboard
Cancel requests
There are often times in a web application where you need to send a request for the latest user interaction. We need a way to prevent subsequent (async) logic from running for all but the most recent request – something like Axios' cancel token or fetch()'s AbortController.
The hardest part about this is that every library has its own API. You can pass a controller to the fetch function but there’s no general way to do it inside SWR. And another question is when to cancel it (when unmounting a component?).
I think it’s way easier to ignore the result than cancel it with the current state of fetching libraries
For now a custom hook can (partially) solve it:
function useCancelableSWR (key, opts) {
const controller = new AbortController()
return [useSWR(key, url => fetch(url, { signal: controller.signal }), opts), controller]
}
// to use it:
const [{ data }, controller] = useCancelableSWR('/api')
// ...
controller.abort()
(also mentioned in #159)
You probably don't want to memoize the AbortController.
You can only abort a controller instance once, so a new request requires a new controller.
Another thing to note is that useMemo
is a performance optimization with no hard guarantee that it won't be recalculated. React can choose to recalculate memoized values (https://reactjs.org/docs/hooks-reference.html#usememo).
@strothj The issue is not whether the AbortController instance is properly cached or not. The abort controller should not be cached at all.
I did this with axios
function useCancellableSWR(key, swrOptions) {
const source = axios.CancelToken.source();
return [
useSWR(key, (url) => axios.get(url, { cancelToken: source.token }).then(res => res.data), {
...swrOptions,
}),
source,
];
}
// usage:
const [{ data }, cancelFn] = useCancellableSWR('/endpoint');
cancelFn.cancel()
Production-ready solution
Note: An AbortController is created with each request
export function useSWRAbort<Data = any, Error = any>(
key: keyInterface,
fn?: fetcherFn<Data>,
options?: ConfigInterface<Data, Error>
): responseInterface<Data, Error> & {
abort: () => void
} {
const aborter = useRef<AbortController>()
const abort = () => aborter.current?.abort()
const res = useSWR<Data, Error>(key, (...args) => {
aborter.current = new AbortController()
return fn?.(aborter.current.signal, ...args)
}, options)
return { ...res, abort }
}
Example
Your fetcher gets an extra param signal
(AbortSignal) before everything else
You can then pass it to your actual fetcher, for example fetch
const { data, error, abort } = useSWRAbort<T>(url, (signal, url) => {
return fetch(url, { signal }).then(res => res.json())
})
return <button onClick={abort}>
...
</button>
Or use it to invalidate data afterwards
const { data, error, abort } = useSWRAbort<T>(url, async (signal, url) => {
const res = await fetch(url)
if (signal.aborted) throw new Error("Aborted")
return await res.json()
})
Test with a timeout
const { data, error, abort } = useSWRAbort<T>(url, async (signal, url) => {
await new Promise(ok => setTimeout(ok, 5000))
console.log("aborted?", signal.aborted)
const res = await fetch(url, { signal })
return await res.json()
})
Does config.isPaused() work for your cases?
useSWR(key, fetcher, {
isPaused() {
return /* condition for dropped requests */
}
})
could this also be solved by a middleware @shuding?
I was able to solve this when using Axios and the middleware like @pke suggests:
https://swr.vercel.app/docs/middleware
let cancelToken;
const cancelMiddleware = (useSWRNext) => {
return (key, fetcher, config) => {
const extendedFetcher = (...args) => {
if (typeof cancelToken !== "undefined") {
cancelToken.cancel("Operation cancelled due to new request.");
}
cancelToken = axios.CancelToken.source();
return fetcher(...args);
};
return useSWRNext(key, extendedFetcher, config);
};
};
Not sure I understand how this works 😉 The cancel token is called immediately?
edit: Ok I get it now. However, shouldn't the cancelToken be defined inside the middleware? Otherwise you can always only have one request going and all others will be cancelled?
One more solution as a middleware that automatically aborts the request as soon as the key is changed or component is unmounted.
const abortableMiddleware: Middleware = (useSWRNext) => {
return (key, fetcher, config) => {
// for each key generate new AbortController
// additionally the key might be serialised using unstable_serialize, depending on your usecases
const abortController = useMemo(() => new AbortController(), [key]);
// as soon as abortController is changed or component is unmounted, call abort
useEffect(() => () => abortController.abort(), [abortController]);
// pass signal to your fetcher in way you prefer
const fetcherExtended: typeof fetcher = (url, params) =>
fetcher(url, { ...params, signal: abortController.signal });
return useSWRNext(key, fetcherExtended, config);
};
};
It also possible to play with this in my sandbox.
I have a new proposal here: #1933, feedback welcome!
One more solution as a middleware that automatically aborts the request as soon as the key is changed or component is unmounted.
const abortableMiddleware: Middleware = (useSWRNext) => { return (key, fetcher, config) => { // for each key generate new AbortController // additionally the key might be serialised using unstable_serialize, depending on your usecases const abortController = useMemo(() => new AbortController(), [key]); // as soon as abortController is changed or component is unmounted, call abort useEffect(() => () => abortController.abort(), [abortController]); // pass signal to your fetcher in way you prefer const fetcherExtended: typeof fetcher = (url, params) => fetcher(url, { ...params, signal: abortController.signal }); return useSWRNext(key, fetcherExtended, config); }; };
It also possible to play with this in my sandbox.
import { useEffect, useRef } from 'react';
import { Middleware, SWRHook, unstable_serialize } from 'swr';
export type CancellablePromise<T> = Promise<T> & {
cancel: (str?: string) => void;
};
export const cancelMiddleware: Middleware =
(useSWRNext: SWRHook) => (key, fetcher, config) => {
const cancelRef = useRef<() => void>();
const keyString = unstable_serialize(key);
const extendsFetcher = fetcher
? (...rest: any) => {
const request = fetcher(...rest) as CancellablePromise<any>;
cancelRef.current = request.cancel;
return request;
}
: fetcher;
const swr = useSWRNext(key, extendsFetcher, config);
useEffect(() => {
return () => {
cancelRef.current?.();
};
}, [keyString]);
return swr;
};
The key needs to be serialized