Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cancel requests #129

Closed
tiagofernandez opened this issue Nov 16, 2019 · 15 comments
Closed

Cancel requests #129

tiagofernandez opened this issue Nov 16, 2019 · 15 comments
Labels
Milestone

Comments

@tiagofernandez
Copy link

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.

@shuding shuding added the feature request New feature or request label Nov 16, 2019
@shuding
Copy link
Member

shuding commented Nov 16, 2019

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?).

@shuding shuding added the discussion Discussion around current or proposed behavior label Nov 16, 2019
@sergiodxa
Copy link
Contributor

I think it’s way easier to ignore the result than cancel it with the current state of fetching libraries

@shuding
Copy link
Member

shuding commented Dec 1, 2019

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)

@AjaxSolutions
Copy link

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.

@strothj
Copy link

strothj commented Jan 31, 2020

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).

@AjaxSolutions
Copy link

@strothj The issue is not whether the AbortController instance is properly cached or not. The abort controller should not be cached at all.

@ekrresa
Copy link

ekrresa commented Apr 4, 2020

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()

@shuding shuding added this to the 1.0 milestone Sep 20, 2020
@hazae41
Copy link

hazae41 commented Feb 26, 2021

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()
})

@huozhi
Copy link
Member

huozhi commented Mar 11, 2021

Does config.isPaused() work for your cases?

useSWR(key, fetcher, {
  isPaused() {
    return /* condition for dropped requests */
  }
})

@huozhi huozhi modified the milestones: 1.0, Backlog Jul 21, 2021
@huozhi huozhi removed the discussion Discussion around current or proposed behavior label Jul 21, 2021
@pke
Copy link

pke commented Feb 26, 2022

could this also be solved by a middleware @shuding?

@ezeikel
Copy link

ezeikel commented Apr 6, 2022

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);
    };
  };

@pke
Copy link

pke commented Apr 6, 2022

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?

@garipov
Copy link

garipov commented May 18, 2022

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.

@shuding
Copy link
Member

shuding commented May 19, 2022

I have a new proposal here: #1933, feedback welcome!

@MyCupOfTeaOo
Copy link

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

@vercel vercel locked and limited conversation to collaborators Dec 20, 2022
@koba04 koba04 converted this issue into discussion #2330 Dec 20, 2022

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
Projects
None yet
Development

No branches or pull requests