When you’re building a real-time, subscription-heavy front-end application, it can be useful to know if your client is actively connected to the server. If that connection is broken, maybe because the server is temporarily down for maintenance, we’d like to be able to show a message explaining the situation to the user. Once we re-establish our connection, we’d like to hide that message and go back to business as usual.

That’s the dream, at least. Trying to implement this functionality using Apollo turned out to be more trouble than we expected on a recent client project.

Let’s go over a few of the solutions we tried that didn’t solve the problem, for various reasons, and then let’s go over the final working solution we came up with. Ultimately, I’m happy with what we landed on, but I didn’t expect to uncover so many roadblocks along the way.

What Didn’t Work

Our first attempt was to build a component that polled for an online query on the server. If the query ever failed with an error on the client, we’d show a “disconnected” message to the user. Presumably, once the connection to the server was re-established, the error would clear, and we’d re-render the children of our component:


const Connected = props => {
  return (
    <Query query={gql'{ online }'} pollInterval={5000}>
      {({error, loading}) => {
        if (loading) {
            return <Loader/>;
        }
        else if (error) {
            return <Message/>;
        }
        else {
            return props.children;
        }
      }}
    </Query>
  );
}

Unfortunately, our assumptions didn’t hold up. Apparently when a query fails, Apollo (react-apollo@2.5.5) will stop polling on that failing query, stopping our connectivity checker dead in its tracks.

NOTE: Apparently, this should work, and in various simplified reproductions I built while writing this article, it did work. Here are various issues and pull requests documenting the problem, merging in fixes (which others claim don’t work), and documenting workarounds:


We thought, “well, if polling is turned off on error, let’s just turn it back on!” Our next attempt used startPolling to try restarting our periodic heartbeat query.


if (error) {
  startPolling(5000);
}

No dice.

Our component successfully restarts polling and carries on refetching our query, but the Query component returns values for both data and error, along with a networkStatus of 8, which indicates that “one or more errors were detected.”

If a query returns both an error and data, how are we to know which to trust? Was the query successful? Or was there an error?

We also tried to implement our own polling system with various combinations of setTimeout and setInterval. Ultimately, none of these solutions seemed to work because Apollo was returning both error and data for queries, once the server had recovered.

NOTE: This should also work, though it would be unnecessary, if it weren’t for the issues mentioned above.


Lastly, we considered leveraging subscriptions to build our connectivity detection system. We wrote a online subscription which pushes a timestamp down to the client every five seconds. Our component subscribes to this publication… And then what?

We’d need to set up another five second interval on the client that flips into an error state if it hasn’t seen a heartbeat in the last interval.

But once again, once our connection to the server is re-established, our subscription won’t re-instantiate in a sane way, and our client will be stuck showing a stale disconnected message.

What Did Work

We decided to go a different route and implemented a solution that leverages the SubscriptionClient lifecycle and Apollo’s client-side query functionality.

At a high level, we store our online boolean in Apollo’s client-side cache, and update this value whenever Apollo detects that a WebSocket connection has been disconnected or reconnected. Because we store online in the cache, our Apollo components can easily query for its value.

Starting things off, we added a purely client-side online query that returns a Boolean!, and a resolver that defaults to being “offline”:


const resolvers = {
    Query: { online: () => false }
};

const typeDefs = gql`
  extend type Query {
    online: Boolean!
  }
`;

const apolloClient = new ApolloClient({
  ...
  typeDefs,
  resolvers
});

Next we refactored our Connected component to query for the value of online from the cache:


const Connected = props => {
  return (
    <Query query={gql'{ online @client }'}>
      {({error, loading}) => {
        if (loading) {
            return <Loader/>;
        }
        else if (error) {
            return <Message/>;
        }
        else {
            return props.children;
        }
      }}
    </Query>
  );
}

Notice that we’re not polling on this query. Any time we update our online value in the cache, Apollo knows to re-render this component with the new value.

Next, while setting up our SubscriptionClient and WebSocketLink, we added a few hooks to detect when our client is connected, disconnected, and later reconnected to the server. In each of those cases, we write the appropriate value of online to our cache:


subscriptionClient.onConnected(() =>
    apolloClient.writeData({ data: { online: true } })
);

subscriptionClient.onReconnected(() =>
    apolloClient.writeData({ data: { online: true } })
);

subscriptionClient.onDisconnected(() =>
    apolloClient.writeData({ data: { online: false } })
);

And that’s all there is to it!

Any time our SubscriptionClient detects that it’s disconnected from the server, we write offline: false into our cache, and any time we connect or reconnect, we write offline: true. Our component picks up each of these changes and shows a corresponding message to the user.

Huge thanks to this StackOverflow comment for pointing us in the right direction.