December 5, 2017

Dynamic GraphQL polling with React and Apollo Client

David Glasser

David Glasser

I recently added a feature to the admin interface on Meteor Galaxy, Meteor Development Group’s hosting platform for Meteor apps, that shows the state of our internal database migrations. I’m using Apollo Client with react-apollo to build Galaxy’s UI, and I wanted the control panel to automatically update to show the state of any migrations. To do that, I had to decide how often the GraphQL query would poll our server.

Photo credit to themtube2 on Flickr.

Now, we’re not usually running any migrations, so a nice, slow polling interval like 30 seconds seemed reasonable. But in the rare case where a migration is running, I wanted to be able to see much faster updates on its progress.

Setting the poll interval from the result of a query

It turns out there’s an easy pattern with Apollo Client and react-apollo to let the poll interval be determined by the data returned by the query itself. I had to figure it out for myself, so I thought I’d share it here to save others the work! I think it’s easier to understand if I use some utilities from the great <a href="https://github.com/acdlite/recompose/blob/master/docs/API.md" target="_blank" rel="noreferrer noopener">recompose</a> library, but you can implement this with the basic React API as well.

The key to this is knowing that the <a href="https://www.apollographql.com/docs/react/basics/queries.html#options-from-props" target="_blank" rel="noreferrer noopener">options</a> parameter to react-apollo’s main <a href="https://www.apollographql.com/docs/react/basics/queries.html#options-from-props" target="_blank" rel="noreferrer noopener">graphql</a> function can itself be a function that depends on its incoming React props. (The options parameter describes the options for the query itself, as opposed to React-specific details like what property name to use for data.) We can then use recompose‘s <a href="https://github.com/acdlite/recompose/blob/master/docs/API.md#withstate" target="_blank" rel="noreferrer noopener">withState</a> to set the poll interval from a prop passed in to the graphql component, and use the <a href="https://reactjs.org/docs/react-component.html#componentwillreceiveprops" target="_blank" rel="noreferrer noopener">componentWillReceiveProps</a> React lifecycle event (added via the recompose <a href="https://github.com/acdlite/recompose/blob/master/docs/API.md#lifecycle" target="_blank" rel="noreferrer noopener">lifecycle</a> helper) to look at the fetched GraphQL data and adjust if necessary.

Here’s how it all fits together:

import { graphql } from "react-apollo";
import gql from "graphql-tag";
import { compose, withState, lifecycle } from "recompose";

const DEFAULT_INTERVAL = 30 * 1000;
const ACTIVE_INTERVAL = 500;

const withData = compose(
  // Pass down two props to the nested component: `pollInterval`,
  // which defaults to our normal slow poll, and `setPollInterval`,
  // which lets the nested components modify `pollInterval`.
  withState("pollInterval", "setPollInterval", DEFAULT_INTERVAL),
  graphql(
    gql`
      query getMigrationStatus {
        activeMigration {
          name
          version
          progress
        }
      }
    `,
    {
      // If you think it's clear enough, you can abbreviate this as:
      //   options: ({pollInterval}) => ({pollInterval}),
      options: props => {
        return {
          pollInterval: props.pollInterval
        };
      }
    }
  ),
  lifecycle({
    componentWillReceiveProps({
      data: { loading, activeMigration },
      pollInterval,
      setPollInterval
    }) {
      if (loading) {
        return;
      }
      if (activeMigration && pollInterval !== ACTIVE_INTERVAL) {
        setPollInterval(ACTIVE_INTERVAL);
      } else if (
        !activeMigration &&
        pollInterval !== DEFAULT_INTERVAL
      ) {
        setPollInterval(DEFAULT_INTERVAL);
      }
    }
  })
);
const MigrationPanelWithData = withData(MigrationPanel);

Note that we check the current value of pollInterval before changing it, because by default in React, nested components will get re-rendered any time we change state, even if you change it to the same value. You can deal with this using <a href="https://reactjs.org/docs/react-component.html#shouldcomponentupdate" target="_blank" rel="noreferrer noopener">componentShouldUpdate</a> or <a href="https://reactjs.org/docs/react-api.html#reactpurecomponent" target="_blank" rel="noreferrer noopener">React.PureComponent</a>, but in this case it’s straightforward just to only set the state when it’s actually changing.

Using this pattern successfully requires at least version 2.0.3 of apollo-client, as earlier versions had a bug related to changing pollInterval.

I hope others find this pattern as useful as I did! This sort of feature is why I find Apollo to be the most delightful way to get data into my apps, even independently of the advantages of GraphQL itself.

Written by

David Glasser

David Glasser

Read more by David Glasser