apollo-client: Problem in unsubscribe of useSubscription react hook
When react component unmounts, the useSubscription hook doesn’t unsubscribe stream in server side.
const TestComponent = () => {
const { loading, error, data } = useSubscription(SUBSCRIPTION);
useEffect(() => {
return () => {
// unsubscribe here
};
}, []);
return <div>TestComponent</div>;
};
About this issue
- Original URL
- State: open
- Created 3 years ago
- Reactions: 10
- Comments: 18 (3 by maintainers)
I was about to read through the test files to check whether there are tests set up (to confirm that
unsubscribeis being called on subscriptions when they ought to be dropped), but then I opened this: https://github.com/apollographql/apollo-client/blob/a565fd5036b23810f59b49affc69a36cdb434a55/src/core/__tests__/QueryManager/index.tsAnd I saw it has 5,881 lines.
And I quietly backed away.
@benjamn Hey, I have the same problem
Upgrading to 3.7.0-beta.1 did not help 😦
We are seeing this in our application as well with lots of leaked subscriptions when using the
useSubscriptionhook. This seems like a pretty fundamental issue, has anyone taken a look?So I’ve looked into this a bit more, and the path that is taken from the user calling
subscription.unsubscribeto the actual sending of theMessageTypes.GQL_STOPmessage is kinda complicated. One could argue that the complexity is needed for the level of abstraction sought – and maybe that is true – but it certainly makes debugging this issue difficult.Here is the path that is taken:
useSubscription: https://github.com/apollographql/apollo-client/blob/a565fd5036b23810f59b49affc69a36cdb434a55/src/react/hooks/useSubscription.ts#L15unsubscribeis called (on the observable stored in the hook): https://github.com/apollographql/apollo-client/blob/a565fd5036b23810f59b49affc69a36cdb434a55/src/react/hooks/useSubscription.ts#L120 2.1) Thatsubscriptionvariable was set here: https://github.com/apollographql/apollo-client/blob/a565fd5036b23810f59b49affc69a36cdb434a55/src/react/hooks/useSubscription.ts#L89 2.2) Thatobservablevariable was set here: https://github.com/apollographql/apollo-client/blob/a565fd5036b23810f59b49affc69a36cdb434a55/src/react/hooks/useSubscription.ts#L28-L39 2.3) Thatclient.subscribefunction is defined here: https://github.com/apollographql/apollo-client/blob/a565fd5036b23810f59b49affc69a36cdb434a55/src/core/ApolloClient.ts#L374-L378 2.4) Which calls this function: https://github.com/apollographql/apollo-client/blob/a565fd5036b23810f59b49affc69a36cdb434a55/src/core/QueryManager.ts#L847 2.5) (which returns this unsubscribe function in the next step)Observablefromzen-observable-ts, which is a reexport of theObservableclass here: https://github.com/zenparsing/zen-observable/blob/328fb66a99242900c0fd4a330e9ff66ecfa3a887/src/Observable.js#L198I unfortunately could not definitively locate the definition of the
unsubscribefunction. But I think it ends up resolving to thisunsubscribemethod in the baseObservableclass: https://github.com/zenparsing/zen-observable/blob/328fb66a99242900c0fd4a330e9ff66ecfa3a887/src/Observable.js#L182-L187Which calls
closeSubscription: https://github.com/zenparsing/zen-observable/blob/328fb66a99242900c0fd4a330e9ff66ecfa3a887/src/Observable.js#L84 …andcleanupSubscription: https://github.com/zenparsing/zen-observable/blob/328fb66a99242900c0fd4a330e9ff66ecfa3a887/src/Observable.js#L59The latter of which appears to call this cleanup function here (back up in the
Concastclass): https://github.com/apollographql/apollo-client/blob/a565fd5036b23810f59b49affc69a36cdb434a55/src/utilities/observables/Concast.ts#L216-L239If you read through the
cleanupfunction above, we might have a piece of the puzzle: as the comment mentions, theConcat.cleanupmethod does not unsubscribe from the underlying observable.Now the comment makes it sound like it’s not the cleanup function’s role to do that unsubscribing.
But the issue is that nowhere in the
Concastclass do I see anything that callsunsubscribe(other than an error handler, which may never get called). And the class is indeed the one that is calling subscribe on the underlying observables: https://github.com/apollographql/apollo-client/blob/a565fd5036b23810f59b49affc69a36cdb434a55/src/utilities/observables/Concast.ts#L207-L210But maybe the base
Observableclass is supposed to be callingunsubscribeon the underlying observables? Maybe.There are some lines in it that call
unsubscribe(though who knows in what circumstances): https://github.com/zenparsing/zen-observable/blob/328fb66a99242900c0fd4a330e9ff66ecfa3a887/src/Observable.js#L229 https://github.com/zenparsing/zen-observable/blob/328fb66a99242900c0fd4a330e9ff66ecfa3a887/src/Observable.js#L239 https://github.com/zenparsing/zen-observable/blob/328fb66a99242900c0fd4a330e9ff66ecfa3a887/src/Observable.js#L345 https://github.com/zenparsing/zen-observable/blob/328fb66a99242900c0fd4a330e9ff66ecfa3a887/src/Observable.js#L390-L391At this point, this is getting too complex for me to follow, so I’ve given up for now. If someone could continue the investigation, it would be appreciated. (perhaps one of the apollo-client maintainers?)
For anyone wanting to take that journey, I think this is the “finish line” where you want the unsubscribe calls to end up: https://github.com/apollographql/subscriptions-transport-ws/blob/11f24136a5e39c6d218d8486a9abff1219a4a565/src/client.ts#L674
That
SubscriptionClientclass fromsubscriptions-transport-wsappears to be instantiated in this apollo-client file: https://github.com/apollographql/apollo-client/blob/a565fd5036b23810f59b49affc69a36cdb434a55/src/link/ws/index.ts#L42And each request (which starts a subscription) passes through that
WebSocketLinkclass’requestmethod here: https://github.com/apollographql/apollo-client/blob/a565fd5036b23810f59b49affc69a36cdb434a55/src/link/ws/index.ts#L50Which is called by
ApolloLink.execute, defined here: https://github.com/apollographql/apollo-client/blob/a565fd5036b23810f59b49affc69a36cdb434a55/src/link/core/ApolloLink.ts#L70Which is called by those lines referenced earlier (these two blocks) in
QueryManager.tsthat instantiate thoseConcatclasses.So from the above, we have a rough idea of what the pathway is; but it was not clear to me from reading through it exactly where the issue is.
That said, my guess is that the problem is somewhere in either apollo-client’s Concast.ts or zen-observable’s Observable.js.
for what it’s worth since I found this through google - though there are mixed opinions online (eg. https://github.com/apollographql/apollo-client/issues/7964#issuecomment-817801295, this SO post) about whether or not
useSubscriptioncleans up after itself, it was not doing this for me. I log my listeners server-side and noticed them accumulating even though those components had unmounted.In my use-case I was using
subscribeToMore. I fixed the cleanup problem by callingsubscribeToMoreinuseEffectand then using its return value as the cleanup function.I don’t know why the docs don’t mention this. https://www.apollographql.com/docs/react/data/subscriptions/#subscribing-to-updates-for-a-query
using apollo-client
3.6.9I experience the same problem 🙁. Refreshing React app with subscriptions active doesn’t send
stopevent to backend.Same issue, useSubscriptions is not resubbing after device is locked. I use expo with apollo-client v3 and hasura as backend.
@zrcni I know that, But it doesn’t
unsubscribethe stream in server side. Note that everything works fine withGraphql playgorundbut not working usinguseSubscriptionhook. Using the hook, stream not stopping or destroying in server side and still continue to stream values.useSubscriptionalready cleans up after itself 🧹 😉 https://github.com/apollographql/apollo-client/blob/5eb624c08ac6bc6ddfa93607793242cebd75f5b1/src/react/hooks/useSubscription.ts#L41