apollo-client
apollo-client copied to clipboard
useSubscription does not get called again when websocket connection is re-established
Intended outcome:
I expect react components that use the useSubscription hook will resubscribe after the websocket connection is re-established
Actual outcome: the subscription is not recreated when the websocket connection is re-established
How to reproduce the issue: Create an apollo client with a web socket link. Add retry logic similar to the following.
const wsLink = new GraphQLWsLink(
createClient({
url: settings?.graphql?.wsUri,
retryAttempts: 5,
// more or less copied directly from https://github.com/enisdenjo/graphql-ws#retry-strategy
retryWait: async () => {
await waitForHealthCheck();
// after the server becomes ready, wait for a second + random 100 - 200 ms timeout
// (avoid DDoSing) and try connecting again, but only in production
const delay = isProduction ? 100 + Math.random() * 100 : 10;
await new Promise((resolve) => setTimeout(resolve, delay));
},
})
);
Create a react component like the following
function ScheduleItem() {
useSubscription(SCHEDULE_CARD_ITEM_UPDATED, { variables: { scheduleCardItemId: _id } });
return <RenderTheScheduleItem />
}
Render the above component on a page.
Start your backend and frontend and see that the subscription works is correctly created. Now, restart your backend and see that the websocket connection is re-established but that the subscription for SCHEDULE_CARD_ITEM_UPDATED is not recreated.
I suspect this is happening because this component is not re-rendered. What is the correct way to re-establish this subscription with apollo? One idea I had was to set a reactiveVar in the websocket retry (and then use that in a useEffect in the component) but I don't think that will work because the apollo client won't be available.
Versions
System:
OS: macOS 12.3.1
Binaries:
Node: 16.14.2 - ~/.nvm/versions/node/v16.14.2/bin/node
Yarn: 1.22.10 - /usr/local/bin/yarn
npm: 8.5.0 - ~/.nvm/versions/node/v16.14.2/bin/npm
Browsers:
Safari: 15.4
npmPackages:
@apollo/client: ^3.5.10 => 3.5.10
apollo3-cache-persist: ^0.14.0 => 0.14.0
I found a way to get closer to my desired solution, but it's still not perfect.
setting up the websocket client:
const wsLink = new GraphQLWsLink(
createClient({
url: settings?.graphql?.wsUri,
retryAttempts: 5,
// more or less copied directly from https://github.com/enisdenjo/graphql-ws#retry-strategy
retryWait: async () => {
await waitForHealthCheck();
// after the server becomes ready, wait for a second + random 100 - 200 ms timeout
// (avoid DDoSing) and try connecting again, but only in production
const delay = isProduction ? 100 + Math.random() * 100 : 10;
await new Promise((resolve) => setTimeout(resolve, delay));
},
on: {
connected: (received) => {
webSocketActiveVar(true);
},
closed: (received) => {
webSocketActiveVar(false);
},
},
})
);
and setting up the component with the subscription:
function ScheduleItem() {
const websocketActive = useReactiveVar(webSocketActiveVar);
const client = useApolloClient();
useEffect(() => {
if (websocketActive) {
client.subscribe({
query: SCHEDULE_CARD_ITEM_UPDATED,
variables: { scheduleCardItemId: _id },
});
}
}, [websocketActive, client]);
return <RenderTheScheduleItem />
}
This seems to recreate the websocket connection and I can see the browser receiving messages but the apollo cache doesn't seem to update when these messages are received. I also wonder if this will cause other problems down the line.
@paymog
Try to use onSubscriptionData
const { loading, error, data } = useSubscription(
SUBSCRIPTION_QUERY,
{
variables: { scheduleCardItemId: _id },
onSubscriptionData: ({ subscriptionData: { data } }) => {
// do something with `data` here
}
},
)
I hope this help as it did help me once I found the answer here
Interesting idea, though I'm not sure how that addresses the issue I'm having. Do you mind elaborating @zebamba?
@zebamba thanks for the suggestion here! @paymog just let us know how this approach works out for you, I'll keep this issue open as a question for now.
My issue that data does not update, but onSubscriptionData always receives updated data.
@jpvajda The same issue occurs for me as well when using subscribeToMore on useQuery.
Happening after iOS device lock (severs socket connection) and unlock (connection re-establishes): the subscription hook never resubscribes. An update or workaround would be appreciated. The very ugly method I have is to set a variable when the subscription errors which I use as the key on the component with the useSubscription hook so it remounts and resubscribes. Not ideal though
Hi @paymog 👋 Would you be able to provide a runnable reproduction of the issue? I'm wondering how you're accessing cache data in order to render items within <RenderTheScheduleItem /> - I'm asking because if RenderTheScheduleItem also calls useSubscription, everything looks like it works as expected on my end.
We have a CodeSandbox that mirrors a forkable GitHub repo and there is a subscriptions.tsx already set up using GraphQLWsLink. Thanks!
@alessbell unfortunately this is almost a year old now and I'm not working on that code anymore and have lost the context.
The client will not trigger retry on events classified as fatal, you can find the list on the lib src/client.ts. You can force retry with this.
{
shouldRetry: () => true,
retryAttempts: 4,
retryWait: async (attempt) => {
console.log("attempt =>: ", attempt);
console.log("Retrying connection in ", (attempt * 7000) / 1000, " seconds");
await new Promise((resolve) => setTimeout(resolve, attempt * 7000));
}
I believe @olarra's suggestion here is the best way to go - it is really up to the websocket library to make a decision if you want to keep reconnecting or give up and terminate the connection for good.
Only in the latter case, useSubscription would stop receiving updates. That said, we recently released version 3.11, which added a restart function to useSubscription that you can call from your component in case you want to reconnect from there.
I believe we cannot really provide any additional features beyond this for this use case, and so I'm going to close this issue for now.
If you are still encountering problems around this that cannot be solved with the upstream library's shouldRetry or useSubscription's restart, please open a new issue so we can start a new discussion there :)
Do you have any feedback for the maintainers? Please tell us by taking a one-minute survey. Your responses will help us understand Apollo Client usage and allow us to serve you better.