apollo-client icon indicating copy to clipboard operation
apollo-client copied to clipboard

Inside StrictMode useSubscription subscribes twice

Open anajavi opened this issue 5 years ago • 42 comments

Intended outcome:

useSubscription should subscribe only once when run inside <StrictMode>

Actual outcome:

useSubscription subscribes twice to the same subscription. I think that this happens because <StrictMode> intentionally runs component bodies twice. I suppose that same thing could happen in concurrent mode too.

This results in double network traffic. Also, the second subscription won't get unsubscribed when the useSubscription is unmounted.

This can be inspected on chrome network tab: usesubscription_chrome in the picture, id 15 and 16 are the single useSubscription and receive the same data. After unmount the id 16 was closed and id 15 was left receiving data(not pictured).

I am not familiar with apollo-client codebase, but I think that this is result of the following line being run in function body instead of inside useEffect: https://github.com/apollographql/apollo-client/blob/23b17fa7283b21966d1c633c52b848f12b4a211e/src/react/hooks/useSubscription.ts#L44

How to reproduce the issue:

Any subscription should behave the same when run in StrictMode.

const SubComponent = ({vesselId}) => {
  const { data } = useSubscription(vesselStatus, {
    variables: { vesselId }
  });
  return null;
}
<StrictMode>
  <SubComponent vesselId="245" />
</StrictMode>

Workaround is to not use StrictMode, but I'm afraid that will result in code that breaks when concurrent mode lands in React.

Versions

System: OS: macOS 10.15.3 Binaries: Node: 12.16.1 - ~/.nvm/versions/node/v12.16.1/bin/node Yarn: 1.22.0 - ~/.nvm/versions/node/v12.16.1/bin/yarn npm: 6.13.4 - ~/.nvm/versions/node/v12.16.1/bin/npm Browsers: Chrome: 80.0.3987.132 Firefox: 73.0.1 Safari: 13.0.5 npmPackages: @apollo/client: ^3.0.0-beta.39 => 3.0.0-beta.39 @apollo/link-ws: ^2.0.0-beta.3 => 2.0.0-beta.3 apollo-server-express: ^2.11.0 => 2.11.0 npmGlobalPackages: apollo-codegen: 0.20.2 apollo: 2.24.0

anajavi avatar Mar 09 '20 22:03 anajavi

If there is interest in fixing this, I can try to make a PR of a failing test case? Not sure it can be done cleanly though.

anajavi avatar Mar 11 '20 07:03 anajavi

I'm having the same issue. <StrictMode /> e.g. seems to be enabled for create-react-app by default and causes the useSubscription hook to subscribe twice on initial render.

Update: strict mode also causes trouble when setting a pollInterval in useQuery which then keeps polling even if the component is unmounted.

flootr avatar Apr 12 '20 07:04 flootr

Update: strict mode also causes trouble when setting a pollInterval in useQuery which then keeps polling even if the component is unmounted.

Both of these hooks will then fail under concurrent mode.

anajavi avatar Apr 14 '20 17:04 anajavi

Yep, running into this issue as well - all of my useSubscription's are subscribing twice.

Tadimsky avatar Apr 30 '20 06:04 Tadimsky

Experiencing the same issue. Any news?

estubmo avatar May 06 '20 13:05 estubmo

Confirmed

esseswann avatar May 25 '20 09:05 esseswann

We have the same problem. Any updates?

csteuer avatar May 29 '20 10:05 csteuer

I have just started playing with Subscriptions. So I have a fresh CRA app here, and it is duplicating subscriptions. When I remove StrictMode, it stops.

brunoreis avatar Jun 14 '20 12:06 brunoreis

Same issue for me

boucherv-project avatar Jul 03 '20 19:07 boucherv-project

Notice that this is true only if you're in development mode. Try to run a production build and this effect will go away.

MaxTibs avatar Jul 07 '20 12:07 MaxTibs

Same thing for me. Wasted a couple of hours. I thought that I was going crazy.

Fxlr8 avatar Jul 08 '20 00:07 Fxlr8

Try to run a production build and this effect will go away.

That might hide the symptoms, but having to run a production build when doing active development would get tedious and annoying real fast. We need a real solution for this issue.

stephencorwin avatar Jul 15 '20 17:07 stephencorwin

I agree, this is annoying. In my case I listen for events and display an alert for each of them in the UI which turns out to be duplicated. However the problem does not come from Apollo-Client, it is React strict mode behavior.. I don't think we can expect a fix coming from Apollo to remove this behavior since it is external to Apollo-Client.

I can see multiple way to limit the side effect of this behavior :

  • Remove the React.StrictMode (not ideal if you want React to check your code)
  • You could write tests for your components to make sure they behave correctly and then turn them off (or not) with global variables on a development environment to avoid the duplicated subscription behavior.
  • If you store data in a cache (e.g. Redux store) or display a table based on the event you receive from a subscription, use a dictionary instead of an array to easily override the duplicated data.

MaxTibs avatar Jul 16 '20 16:07 MaxTibs

I agree, this is annoying. In my case I listen for events and display an alert for each of them in the UI which turns out to be duplicated. However the problem does not come from Apollo-Client, it is React strict mode behavior.. I don't think we can expect a fix coming from Apollo to remove this behavior since it is external to Apollo-Client.

There is a reason for strict mode rendering twice. It is to prevent bugs in the coming concurrent mode. So this should be fixed in apollo-client.

anajavi avatar Jul 16 '20 19:07 anajavi

The worst thing is that those duplicated observables leak and never get destroyed, so when you do client.resetStore() all of them get refetched and this most probably will throw errors because the auth token is not set anymore. I'm surprised that this issue is completely ignored for almost half a year already.

igor10k avatar Aug 28 '20 00:08 igor10k

Screenshot 2020-09-07 at 15 14 50 I'm still getting this issue, returning twice when is triggered the subscription

semoal avatar Sep 07 '20 22:09 semoal

experiencing this now, seeing duplicate subscriptions. First thing I thought of was to use useEffect, then I found this thread.

davegariepy avatar Sep 18 '20 21:09 davegariepy

im having this issue also

raymclee avatar Oct 18 '20 21:10 raymclee

I have the same issue

neildotvn avatar Oct 23 '20 07:10 neildotvn

The same issue

ybatsiun avatar Nov 02 '20 14:11 ybatsiun

Same issue

MR-Neto avatar Nov 04 '20 15:11 MR-Neto

same issue here, please help

khaphannm avatar Nov 12 '20 07:11 khaphannm

https://github.com/apollographql/subscriptions-transport-ws/blob/master/src/client.ts

can't wrap my head around why this is happening but in DEV mode

this.applyMiddlewares (which returns a promise)

seems to be resolving twice for the same subscription BUT... with a different id

private executeOperation(options: OperationOptions, handler: (error: Error[], result?: any) => void): string {
    if (this.client === null) {
      this.connect();
    }

    const opId = this.generateOperationId();
    this.operations[opId] = { options: options, handler };

    this.applyMiddlewares(options)
      .then(processedOptions => {
        this.checkOperationOptions(processedOptions, handler);
        if (this.operations[opId]) {
          this.operations[opId] = { options: processedOptions, handler };
          this.sendMessage(opId, MessageTypes.GQL_START, processedOptions);
        }
      })
      .catch(error => {
        this.unsubscribe(opId);
        handler(this.formatErrors(error));
      });

    return opId;
}

the odd thing is, even though this condition exists

if (this.operations[opId]) {
      this.operations[opId] = { options: processedOptions, handler };
      this.sendMessage(opId, MessageTypes.GQL_START, processedOptions);
}

AND opId is created only once BEFORE calling

this.applyMiddlewares(...)

The second time the promise is resolved ...the same condition evaluates to true for an id that is not created and shouldn't exist in this.operations

didn't dig too much through the code, so this... could be totally unrelated to why the subscription is fired twice but maybe its a good starting point

kelvin2200 avatar Nov 29 '20 11:11 kelvin2200

👍 same issue

Dezzymei avatar Dec 02 '20 18:12 Dezzymei

I also can confirm the issue. Does someone have an idea of why this is happening? I don't really understand the relationship between <React.StrictMode/> and the subscriptions...

matbour avatar Dec 16 '20 13:12 matbour

I also can confirm the issue. Does someone have an idea of why this is happening? I don't really understand the relationship between <React.StrictMode/> and the subscriptions...

Because in development StrictMode causes function component bodies to double-execute: https://reactjs.org/docs/strict-mode.html#detecting-unexpected-side-effects

The useSubscription hook is breaking contract with react by starting the subscription in function body.

anajavi avatar Dec 16 '20 13:12 anajavi

I also can confirm the issue. Does someone have an idea of why this is happening? I don't really understand the relationship between <React.StrictMode/> and the subscriptions...

Because in development StrictMode causes function component bodies to double-execute: https://reactjs.org/docs/strict-mode.html#detecting-unexpected-side-effects

The useSubscription hook is breaking contract with react by starting the subscription in function body.

Thank you very much for the precision. I'll try to look on the useSubscription hook and maybe submit a PR to fix this.

matbour avatar Dec 19 '20 23:12 matbour

I get multiple as well

sgentile avatar Dec 29 '20 17:12 sgentile

Saw the same thing happening earlier removing StrictMode resolved it.

joshbedo avatar Jan 03 '21 02:01 joshbedo

StrictMode also affects reactive variables for me. I have cache policy field that reads rv. When changed rv's value is updated, broadcast to cache, onNewData call triggers, but deep-memo'd state does not update (or perhaps it does in first render, clearing some internal dirty flags, and skips doing so in second strict mode render).

Xazzzi avatar Feb 15 '21 11:02 Xazzzi