Skip to content
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

Feature idea: Abort pending requests #362

Open
klis87 opened this issue Aug 8, 2018 · 62 comments
Open

Feature idea: Abort pending requests #362

klis87 opened this issue Aug 8, 2018 · 62 comments
Labels
project-apollo-client (legacy) LEGACY TAG DO NOT USE

Comments

@klis87
Copy link

klis87 commented Aug 8, 2018

Migrated from: apollographql/apollo-client#1541

I think that request aborts and other things from migated topics like options borrowed from Redux-Saga, for example takeLatest (cancelling previous request if there is new pending) could be easier implemented once race lands into Apollo Links, like mentioned in https://www.apollographql.com/docs/link/composition.html

@matthargett
Copy link

This is really critical on systems where the pool of sockets is limited, and where CPU is limited. If the user has navigated away before we have the data, ideally the socket (or stream in the case of H/2, or subscription in the case of pubsub like redis) gets closed and cleaned up immediately so a queued request that was waiting for a free socket can get its turn. Additionally, JSON/responses for a cancelled request shouldn’t take up CPU time being parsed/deserialized. This was a key optimization with redux + RxJS allowed for in one of the teams I worked on.

@SirwanAfifi
Copy link

Any progress on this? In our project we have a react component to upload multiple images, now we want to add image cancelation ability, I had a look at the source code, it seems that we can pass a fetchOptions using mutation function:

const { mutation: uploadImg } = uploadImageObject;
uploadImg({
   variables: {
     file
   },
   context: {
     fetchOptions: {
      signal,
     }
   }
});

But when I trigger the abort controller, nothing happens, it actually doesn't stop pending request. Any idea?

@rico345100
Copy link

How to use this? How to use stopQuery and what is queryId? Could you make some explanation or simple example?

@amcdnl
Copy link

amcdnl commented Jan 28, 2019

+1 for abort - for things like typeahead this reduces race cases.

@firasdib
Copy link

Does anyone have any more information about this? I'm seeing the same issue as @SirwanAfifi.

@januszhou
Copy link

januszhou commented May 6, 2019

this works for me at [email protected]

const abortController = new AbortController();
client.query({ 
  query,
  variables,
  context: { 
    fetchOptions: { 
      signal: abortController.signal 
    }
});
// later on
abortController.abort();

EDIT: This solution works for the first time, but it doesn't cancel for the second request, looks like apollographql/apollo-client#4150 had a solution(not tested yet)

@gabor
Copy link

gabor commented May 13, 2019

check this discussion apollographql/apollo-client#4150 (comment) , it has a solution for canceling requests.

@mrdulin
Copy link

mrdulin commented Aug 29, 2019

My case is when I use schema stitching, here is my code:

const http = ApolloLink.from([
    new RetryLink({
      attempts: {
        max: 5,
        retryIf: (error, operation: Operation) => {
          logger.error(`retry to connect error: ${error.message}`);
          return !!error;
        },
      },
      delay: {
        initial: 500,
        jitter: true,
        max: Infinity,
      },
    }),
    new HttpLink({ uri, fetch }),
  ]);

  const schemaOriginal = makeRemoteExecutableSchema({
    schema: await introspectSchema(http),
    link: http,
  });

When the remote service is down or not found, I will retry 5 times and after that, I want to stop/abort retrying.

@mnpenner
Copy link

mnpenner commented Aug 29, 2019

@januszhou Where are you seeing fetchOptions as an option? https://www.apollographql.com/docs/react/api/apollo-client/#ApolloClient.query

Edit: It does seem to work.

@januszhou
Copy link

@januszhou Where are you seeing fetchOptions as an option? https://www.apollographql.com/docs/react/api/apollo-client/#ApolloClient.query

Edit: It does seem to work.

My solution turned out doesn't work the second request, looks like apollographql/apollo-client#4150 has a better solution(I haven't tested it yet)

@mnpenner
Copy link

mnpenner commented Sep 4, 2019

@januszhou Thanks! I was noticing some weirdness when I did this. I had a .finally() which was never been called after I aborted a query, probably because it was never being re-executed.

@NikitaDev14
Copy link

I had a problem with race condition using RxJs+Apollo and I created a simple solution, hope it will be useful.
graphql-express

@bgultekin
Copy link

It looks like also client.stop() helps for aborting pending requests.

const client = new ApolloClient({
	link: new HttpLink({
		uri: 'http://graphql.url'
	}),
	queryDeduplication: false
});

// some queries run

client.stop();

If you don't want to abort everything sent through the client, you can use client.queryManager.stopQuery(queryId) (this is what client.stop() uses at the end and it is reachable, but haven't tested).

@devdudeio
Copy link

How can I get the current queryId ?

@bgultekin
Copy link

@RLech Haven't tested but it seems like you can check client.queryManager.queries map after sending a query. You may find a better answer in the QueryManager class (https://github.com/apollographql/apollo-client/blob/65eef3730e8b3d2b66ca0fe6d88d0b579a4d31ea/packages/apollo-client/src/core/QueryManager.ts).

@hueter
Copy link

hueter commented Dec 11, 2019

This would be incredibly useful and powerful as part of the hooks API, for instance something like:

const { data, loading, fetchMore, refetch, cancel } = useQuery(ANY_QUERY);

useEffect(() => {
  return () => {
    // aka componentWillUnmount
    cancel();
  };
}, [cancel]);

Without this, as others have mentioned, typeahead components (e.g. ReactSelect) and other async components are susceptible to the Warning: Can't call setState (or forceUpdate) 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 the componentWillUnmount method. issue

@devdudeio
Copy link

@hueter yeah I was looking for something like this. Would be awesome when we see this in the near future.

@TSMMark
Copy link

TSMMark commented Dec 19, 2019

What would it take to implement? Is anyone already taking a try at a PR?

@alflennik
Copy link

I need this.

We have a feature where admins can log in as different users, and when they do we invalidate and refetch the GraphQL graph by calling await client.resetStore(). This does clear and refetch all the completed queries, but the problem is that there are pending requests which will come back with data for the wrong user!

I am so sure that I can call some of the APIs on the client to invalidate the requests, but everything I've tried hasn't worked. Even if there is a low-level API I can use, this seems like a pretty natural issue to run into, so a high level API would be save the next person a lot of effort.

bchrobot referenced this issue in politics-rewired/Spoke Feb 11, 2020
Callbacks from the pagination components were triggering changes to the props they were passed.
This in turn kicked off another fetch by the HOC and so on. Fix this by ensuring props have
actually changed before making queries again. Make the queries directly from the Apollo client
rather than a HOC in order to have better control over when the queries are kicked off.

Ideally changes to props would cancel an existing query, but this is not really supported in
apollo-client at the moment. There are partial workarounds using AbortController and .watchQuery()
but neither work for us. See:
https://github.com/apollographql/apollo-feature-requests/issues/40#issuecomment-489487888

This also fixes a state comparison bug in AdminIncomingMessageList and fixes an Apollo complain
about a missing organization id.
@qortex
Copy link

qortex commented Feb 23, 2020

Faced the same issue with fetchMore (implementing a typeahead for "search" field: without debounce on the keyboard inputs, some requests have race conditions which brought up the issue).

I did not find a way to gracefully plug a switchMap because the updateQuery function is called directly and not through an Observable chain - which is unfortunate.

I did the following ugly hack, which works in my case because I can keep track of the sent fetchMore queries. I add an artificial new random variable to all fetchMore queries I do and remember what was the last one. I accept the results in updateQuery only if the result comes from the last one.

this.lastFilterQuerySent = Math.floor(Math.random() * 100000000);

targetQuery.fetchMore({
      variables: {
        // ... other variables
        _internalSendingId: this.lastFilterQuerySent,
      },
      updateQuery: (prev, result) => {
        if (
          !result.fetchMoreResult ||
          result.variables._internalSendingId !== this.lastFilterQuerySent
        ) {
          // discard since it's not the last query
          return prev;
        }
        // ok take into account
        return result.fetchMoreResult;
      },
    });

It does the trick, but with several serious drawbacks:

  • it mixes imperative & reactive
  • if the last query fails, all other will be missed too
  • it adds a virtual new useless variable
  • in doing so, it breaks any caching heuristic
  • it does not cancel the in-flight queries, it just ignores non-last ones

Really, just a "cancel inflight" would be great, since the checkInFlight attribute properly pinpoints when a request for this query is already flying - but stop stops the query entirely, making it impossible to follow up with another call.

Or a simple way to plug a switchMap call based on when the fetchMore call takes place.

(Unsubscribing from the observable as suggested is not acceptable for my use case, since I don't own the subscribers to the data source in this context).

@qortex
Copy link

qortex commented Feb 24, 2020

Actually, the hack above can be improved: no need to store the internalSendingId in the query variables: taking advantage of closures and local variables scopes, it can be simplified to:

this.lastFilterQuerySent = Math.floor(Math.random() * 100000000);
const localQuerySentId = this.lastFilterQuerySent;

targetQuery.fetchMore({
      variables: {
        // ... other variables
      },
      updateQuery: (prev, result) => {
        if (
          !result.fetchMoreResult ||
          localQuerySentId !== this.lastFilterQuerySent
        ) {
          // discard since it's not the last query
          return prev;
        }
        // ok take into account
        return result.fetchMoreResult;
      },
    });

It still smells, but a bit less: caching strategies can apply normally because the variables are not touched (although it seems fetchMore calls are never cached as far as I can see in my testing examples).

@dannycochran
Copy link

dannycochran commented Mar 27, 2020

For the useQuery abort case, there's a fairly complex way of making this work with your Apollo Client if anyone is looking for short term relief. This is what I'm doing and it works well for the use case where I want to cancel previous queries from a text input.

  1. First, set up your apollo client with an observable that will keep track of in flight requests and if a new request comes in from the same component, cancel the previous one:
const requestLink = new ApolloLink(
  (operation, forward) =>
    new Observable(observer => {
      // Set x-CSRF token (not related to abort use case)
      let handle: ZenObservable.Subscription | undefined;
      Promise.resolve(operation)
        .then(oper => request(oper))
        .then(() => {
          handle = forward(operation).subscribe({
            next: observer.next.bind(observer),
            error: observer.error.bind(observer),
            complete: observer.complete.bind(observer),
          });
        })
        .catch(observer.error.bind(observer));

      const context = operation.getContext();
      const requestId = uuid();

      if (context.abortPreviousId) {
        const controller = new AbortController();

        operation.setContext({
          ...context,
          controller,
          fetchOptions: {
            signal: controller.signal,
          },
        });

        Object.values(inFlightOperations).forEach(operationData => {
          if (operationData?.abortPreviousId === context.abortPreviousId) {
            // If a controller exists, that means this operation should be aborted.
            operationData?.controller?.abort();
          }
        });

        inFlightOperations[requestId] = {
          controller: controller,
          abortPreviousId: context.abortPreviousId,
        };
      }

      return () => {
        // We don't need to keep around the requests, remove them once we are finished.
        delete inFlightOperations[requestId];

        if (handle) {
          handle.unsubscribe();
        }
      };
    })
);
  1. Then, you just need to pass in some unique identifier for your component. I have a wrapped useQuery function so I don't need to create the ID myself in every component, and rather, I just specify "abortPrevious" as true from components. But this works as well:
const Foo = () => {
  const abortPreviousId = useRef(uuid());
  const { data, loading, error } = useQuery(SomeQuery, {
    context: {
      // Make sure we only abort queries from this particular component, another
      // component might be using the same query simultaneously and doesn't want
      // to be aborted.
      abortPreviousId: abortPreviousId.current,
    },
  });
  return <p>bar</p>;
};

@dylanwulf
Copy link

@dannycochran Thank you for sharing!

@guilhermeKodama
Copy link

guilhermeKodama commented Apr 25, 2020

Any news on this one. It seems there are a lot of hacky ways to do it but any oficial PR?

@blocka
Copy link

blocka commented May 24, 2020

@dannycochran what is the request function?

@memark
Copy link

memark commented Aug 14, 2020

This is needed!

@klis87
Copy link
Author

klis87 commented Aug 14, 2020

Because more than 2 years passed since raising this issue, if anyone is interested, I developed a library similar to Apollo - redux-requests, but for any API - not only GraphQL - with native and automatic abort support. It has also other features, like caching, optimistic updates, automatic normalisation, Redux level SSR and so on. For people who care about requests aborts, I recommend to check it out here on github. Don't be scared with Redux, as in practice you need to write just actions and use built-in selectors.

@nirus
Copy link

nirus commented Sep 14, 2020

(Solved)

After struggling for day i was finally able to get the solution. People who are looking for the answers to cancel the pending request, i have documented the POC code & solution walkthrough step by step in this stackoverflow post (Read along.)

Thanks to this discussion thread. It helped a lot to arrive at the solution.

Credits to @dannycochran & @bgultekin solutions in this thread.

Hope this helps someone out there.

@julkue
Copy link

julkue commented Sep 16, 2020

I also really need this to have the possibility to mark several filters as checked and only take continue with the latest requrest but cancel any earlier requests. While I was able to .unsubscribe() on a watchQuery to make sure my code isn't execute redundantly (see e.g. apollographql/apollo-client#4150 (comment)) the requests will still be handled by the browser. What I really need is a way to cancel the request on a network layer side.

@hwillson hwillson transferred this issue from apollographql/apollo-feature-requests Sep 28, 2021
@joshjg
Copy link

joshjg commented Oct 6, 2021

@hwillson Although the custom AbortController approach does cancel the requests, it also causes this issue:
apollographql/apollo-client#4150

In the discussion there, it was recommended not to take this approach:
apollographql/apollo-client#4150 (comment)

The suggested workaround in that issue is to use watchQuery + unsubscribe - but that's not really feasible for our use case in which we want to cancel ALL pending requests and selectively refetch only a subset.

The only way to do this that I've found is an ugly hack, reaching into Apollo's internals like this:

client.queryManager.inFlightLinkObservables = new Map();
// Now refetch certain queries

Edit: related question, should calling refetch multiple times in a row, before the previous request is complete, cancel the previous request? Currently, subsequent refetch calls seem to do nothing - they simply return the in-flight Promise.

@digeomel
Copy link

digeomel commented Oct 9, 2021

@joshjg I don't understand why you can't kill ALL requests using watchQuery + unsubscribe. Why can't you create a service to store all your active subscriptions and unsubscribe them there?

@joshjg
Copy link

joshjg commented Nov 24, 2021

@digeomel We have a large app using Apollo's react hooks extensively - we aren't often calling client.query directly where it could be swapped out easily.

@digeomel
Copy link

@joshjg apologies, it wasn't clear to me that you're using React. However, I suspect that your "ugly hack" solution can result in memory leaks, as it seems that your existing subscriptions are not properly unsubscribed.

@FreTimmerman
Copy link

@nirus

(Solved)

After struggling for day i was finally able to get the solution. People who are looking for the answers to cancel the pending request, i have documented the POC code & solution walkthrough step by step in this blog post (Read along.)

Thanks to this discussion thread. It helped a lot to arrive at the solution.

Credits to @dannycochran & @bgultekin solutions in this thread.

Hope this helps someone out there.

This is great. but do i understand correctly that this only cancels a request when a new request with the same id gets made?
Can i cancel a request without having to send a new one?

@flippidippi
Copy link

flippidippi commented Mar 3, 2022

Is this issue just for manually aborting requests?

We are having an issue where we are using useQuery, the variables are changing, but the previous request that is not complete is not being canceled.

Note, it does seem to work on component unmounting though.

@levrik
Copy link

levrik commented Mar 4, 2022

@flippidippi We're having this exact same issue. I thought this issue is about implementing this and not about manual cancellation but you seem to be right.

Is there a tracking issue about aborting requests when variables are changing?

@flippidippi
Copy link

@levrik I'm not sure? Which is why I was asking if this issue covered this. In the original issue, it says

cancelling previous request if there is new pending

which seems like that should be about when variables change, but most of the comments under here seem like they are addressing the more manual approach.

@hwillson should this be a separate issue?

@levrik
Copy link

levrik commented Mar 4, 2022

@flippidippi Sorry. Question about if an issue already exists wasn't directed to you, thus the line break 😅

@gmavritsakis
Copy link

Hi everyone.
I am using "@apollo/client": "^3.5.6" and nothing worked properly for me.
Also, I am using @graphql-codegen and I don't have access to the underlying observables.

So, after gathering the information from various sources I came up with the following solution which seems to be working good for my case. I used the base structure from here but changed the way it works so as to remove the AbortController which was creating problems (the cancelationLink was not working properly after the first cancellation, here is explained why).

Actually, what I do, is store the previous observer and throw an error to it which cancels that previous request when a new request comes up for the same operation.

This behavior gets applied automatically to all calls. If the user wants to prevent it for some calls he will have to pass a
preventCancelations: true option inside the context.

I am not 100% sure that it does not create some kind of memory leaks or other subscription based issues, but I have been using this for some time now, without any obvious issues.

If anyone has the time please verify.


import { ApolloLink, Observable } from '@apollo/client';

const connections: { [key: string]: any } = {};

export const cancelRequestLink = new ApolloLink(
	(operation, forward) =>
		new Observable(observer => {
			const context = operation.getContext();
			const { operationName } = operation;

			const connectionHandle = forward(operation).subscribe({
				next: (...arg) => observer.next(...arg),
				error: (...arg) => {
					delete connections[operationName];
					observer.error(...arg);
				},
				complete: (...arg) => {
					delete connections[operationName];
					observer.complete(...arg);
				}
			});

			if (!context.preventCancelations) {
				if (connections[operationName]) {
					connections[operationName].error(new Error('Request aborted'));
					delete connections[operationName];
				} 

				connections[operationName] = observer;
			}
			return connectionHandle;
		})
);

@dominikklein
Copy link

I there a planned in-house solution for canceling requests when the refetch-function is used?

@LevanArabuli
Copy link

LevanArabuli commented Dec 5, 2022

Hi all - as mentioned in https://github.com/apollographql/apollo-feature-requests/issues/40#issuecomment-489487888 this functionality is already supported in Apollo Client. We like the hook cancel suggestion in https://github.com/apollographql/apollo-feature-requests/issues/40#issuecomment-564704980 and will be looking into this. Thanks for the great discussion!

@hwillson Any updates regarding this? Has this been implemented?

@dengunya
Copy link

Hi all - as mentioned in https://github.com/apollographql/apollo-feature-requests/issues/40#issuecomment-489487888 this functionality is already supported in Apollo Client. We like the hook cancel suggestion in https://github.com/apollographql/apollo-feature-requests/issues/40#issuecomment-564704980 and will be looking into this. Thanks for the great discussion!

@hwillson Any update? Was it implemented? Or rejected?
It's kinda important issue without any progress for years already ;(

@maxhaice2
Copy link

maxhaice2 commented Mar 15, 2023

Still waiting for updates @hwillson, abortcontroller, unsubscribing doesn't work (angular)

@alessbell alessbell transferred this issue from apollographql/apollo-client Apr 28, 2023
@alessbell alessbell added the project-apollo-client (legacy) LEGACY TAG DO NOT USE label Apr 28, 2023
@eric-ho-github
Copy link

for those using reactjs, I ended up creating a new hook for each hook of apollo while waiting for official support.
Here is my example useCancellableLazyQuery

import {
  DocumentNode,
  LazyQueryExecFunction,
  LazyQueryHookOptions,
  OperationVariables,
  QueryResult,
  TypedDocumentNode,
  useLazyQuery,
} from '@apollo/client';
import { mergeDeep } from '@apollo/client/utilities';
import React from 'react';

type CancellableLazyQueryResultTuple<
  TData,
  TVariables extends OperationVariables = OperationVariables,
> = [
  LazyQueryExecFunction<TData, TVariables>,
  QueryResult<TData, TVariables> & { cancelQuery: () => void },
];

export function useCancellableLazyQuery<
  TData,
  TVariables extends OperationVariables = OperationVariables,
>(
  query: DocumentNode | TypedDocumentNode<TData, TVariables>,
  options?: LazyQueryHookOptions<TData, TVariables>,
): CancellableLazyQueryResultTuple<TData, TVariables> {
  const abortController = React.useRef(new AbortController());
  const [execFunc, queryResult] = useLazyQuery(query, options);

  const enhancedQueryExecFunc: LazyQueryExecFunction<TData, TVariables> = (options) => {
    const mergedOptions = mergeDeep(options, {
      context: {
        fetchOptions: {
          signal: abortController.current.signal,
        },
      },
    });

    return execFunc(mergedOptions);
  };

  const cancelQuery = () => {
    abortController.current.abort();
    abortController.current = new AbortController();
  };

  return [enhancedQueryExecFunc, { ...queryResult, cancelQuery }];
}

@sadanandpai
Copy link

This is such an important feature and still missing in the lib. Any updates on this

@PrakashC1
Copy link

Hi, Any update on this feature?

@jerelmiller
Copy link
Member

Hey all 👋 Thanks for your patience on this issue!

We've had the ability to abort requests for some time via fetchOptions: { signal } as some have alluded to in the comments above, though we had discovered some issues with it in versions < 3.7. 3.8.0 added two fixes for this feature (apollographql/apollo-client#11040, apollographql/apollo-client#11058) that make this feature behave more as expected.

Since this issue has been around for quite a while at this point, it feels like its become a mix of several ideas and bug reports and it has become hard to understand exactly what is being asked for. For those that have experienced bugs with this in the past, would you be willing to try and more recent version to ensure the issues have been resolved? If not, please open a bug report in the core apollo-client repo and we'd be happy to dig in further.

As for the others, I see some comments asking for updates and others suggesting this feature is missing. Are you specifically looking for aborting requests in general, or are you waiting for a cancel like function returned from the hooks? Any help would be appreciated. Thanks!

@quocluongha
Copy link

Thank you so much for finally looking into this.

I find myself do need a custom function like cancel to be returned from the hooks.

My use case lies specifically on React Native app navigation. When a screen is out of focus, I want to cancel some in-flight network requests.

@hueter
Copy link

hueter commented Jan 2, 2024

Hey all 👋 Thanks for your patience on this issue!

We've had the ability to abort requests for some time via fetchOptions: { signal } as some have alluded to in the comments above, though we had discovered some issues with it in versions < 3.7. 3.8.0 added two fixes for this feature (apollographql/apollo-client#11040, apollographql/apollo-client#11058) that make this feature behave more as expected.

Since this issue has been around for quite a while at this point, it feels like its become a mix of several ideas and bug reports and it has become hard to understand exactly what is being asked for. For those that have experienced bugs with this in the past, would you be willing to try and more recent version to ensure the issues have been resolved? If not, please open a bug report in the core apollo-client repo and we'd be happy to dig in further.

As for the others, I see some comments asking for updates and others suggesting this feature is missing. Are you specifically looking for aborting requests in general, or are you waiting for a cancel like function returned from the hooks? Any help would be appreciated. Thanks!

hey there! 👋🏻

I posted about using a cancel method from the hook in 2019 above and still think that being able to invoke it declaratively in client-side code would be super useful and complementary to the existing APIs like refetch and fetchMore, which are sort of related to cancel as a form of "query control".

In the past 4 years with Apollo client, I have run into a lot of valid business logic cases (user actions such as navigating, type-aheads, or even literally clicking a "cancel" button) that are based on discrete events in the UI where I'd invoke cancel if I could. Often I just wrap things in lodash's debounce method to cancel, but it won't work if the request has already kicked off.

I'm not sure if I'm interpreting Apollo's AbortController functionality you've linked to correctly, but it seems like those are more global link config settings rather than related to specific query invocations?

I also found a similar discussion in SWR's repo which proposes a thin wrapper around built-in fetch's AbortController but looks like you can access it at the hook's call site.

@jerelmiller
Copy link
Member

@hueter thanks so much for the response! This is useful information!

it seems like those are more global link config settings rather than related to specific query invocations?

You can absolutely do this local to the query you're using by passing the signal to fetchOptions on context, which all query hooks expose.

// assuming you need to keep the same controller around for every render
const controllerRef = useRef(new AbortController());

useQuery(QUERY, {
  context: {
    fetchOptions: {
      signal: controllerRef.current.signal
    }
  }
});

// Abort the request when this button is clicked
<button onClick={() => controllerRef.current.abort()}>Abort request</button>

If your component sticks around and could potentially abort the signal more than once, you'll need to reinstantiate the controller each time you abort since the controller only allows you to abort a single time.

const controllerRef = useRef(new AbortController());

const cancel = () => {
  controllerRef.current.abort();
  controllerRef.current = new AbortController();
}

useQuery(QUERY, {
  context: {
    fetchOptions: {
      signal: controllerRef.current.signal
    }
  }
});

// Use the `cancel` function instead to ensure the controller is replaced after aborting.
// The latest `signal` will be passed the next time `useQuery` renders.
<button onClick={() => cancel()}>Abort request</button>

(this is essentially the solution mentioned above)

This works for any core API as well (such as client.query(...), client.watchQuery(...), etc).

@quocluongha I'm curious if the above would work in your situation? In your case, you should be able to just use that cancel function in your useEffect cleanup:

const cancel = useCallback(() => {
  // ...
}, [])

useEffect(() => {
  return cancel;
}, [cancel]);

This should be a good solution for you without the support of a core API.


All this said, I don't disagree that an exported cancel function would give a nice shortcut to the above so that you wouldn't have to manage the lifecycle of your own AbortController, though if we provide this, I think there are additional considerations we should probably make to ensure this works as expected in all cases. Some things I can think of off the top of my head:

@quocluongha
Copy link

quocluongha commented Jan 3, 2024

@jerelmiller Yes, the solution worked.

Btw there some advanced use cases such as dealing with race condition (e.g typeahead search). As this PO said, it would be nice if the library also support cancellation behaviors like takeLatest (Redux Saga) or switchMap (RxJS) (accept the latest request and cancel all previous pending request) and takeLeading (Redux Saga) or exhaustMap (RxJS) (ignore all incoming request until the first one is finished) internally or via a race link.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
project-apollo-client (legacy) LEGACY TAG DO NOT USE
Projects
None yet
Development

No branches or pull requests