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
unsubscribe
is 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
useSubscription
hook. 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.unsubscribe
to the actual sending of theMessageTypes.GQL_STOP
message 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#L15unsubscribe
is called (on the observable stored in the hook): https://github.com/apollographql/apollo-client/blob/a565fd5036b23810f59b49affc69a36cdb434a55/src/react/hooks/useSubscription.ts#L120 2.1) Thatsubscription
variable was set here: https://github.com/apollographql/apollo-client/blob/a565fd5036b23810f59b49affc69a36cdb434a55/src/react/hooks/useSubscription.ts#L89 2.2) Thatobservable
variable was set here: https://github.com/apollographql/apollo-client/blob/a565fd5036b23810f59b49affc69a36cdb434a55/src/react/hooks/useSubscription.ts#L28-L39 2.3) Thatclient.subscribe
function 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)Observable
fromzen-observable-ts
, which is a reexport of theObservable
class here: https://github.com/zenparsing/zen-observable/blob/328fb66a99242900c0fd4a330e9ff66ecfa3a887/src/Observable.js#L198I unfortunately could not definitively locate the definition of the
unsubscribe
function. But I think it ends up resolving to thisunsubscribe
method in the baseObservable
class: 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
Concast
class): https://github.com/apollographql/apollo-client/blob/a565fd5036b23810f59b49affc69a36cdb434a55/src/utilities/observables/Concast.ts#L216-L239If you read through the
cleanup
function above, we might have a piece of the puzzle: as the comment mentions, theConcat.cleanup
method 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
Concast
class 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
Observable
class is supposed to be callingunsubscribe
on 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
SubscriptionClient
class fromsubscriptions-transport-ws
appears 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
WebSocketLink
class’request
method 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.ts
that instantiate thoseConcat
classes.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
useSubscription
cleans 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 callingsubscribeToMore
inuseEffect
and 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.9
I experience the same problem 🙁. Refreshing React app with subscriptions active doesn’t send
stop
event 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
unsubscribe
the stream in server side. Note that everything works fine withGraphql playgorund
but not working usinguseSubscription
hook. Using the hook, stream not stopping or destroying in server side and still continue to stream values.useSubscription
already cleans up after itself 🧹 😉 https://github.com/apollographql/apollo-client/blob/5eb624c08ac6bc6ddfa93607793242cebd75f5b1/src/react/hooks/useSubscription.ts#L41