While working on a recent client project, Estelle and I ran into a fun Apollo quirk. It turns out that an Apollo query with an active pollInterval won’t respect new variables provided by calls to refetch.

To demonstrate, imagine we’re rendering a paginated table filled with data pulled from the server:


const Table = () => {
    let { data } = useQuery(gql`
        query items($page: Int!) {
            items(page: $page) {
                pages
                results {
                    _id
                    result
                }
            }
        }
    `, {
        pollInterval: 5000
    });
    
    return (
        <>
            <table>
                {data.items.results.map(({ _id, result }) => (
                    <tr key={_id}>
                        <td>{result}</td>
                    </tr>
                ))}
            </table>
        </>
    );
};

The items in our table change over time, so we’re polling our query every five seconds.

We also want to give the user buttons to quickly navigate to a given page of results. Whenever a user presses the “Page 2” button, for example, we want to refetch our query with our variables set to { page: 2 }:


 const Table = () => {
-    let { data } = useQuery(gql`
+    let { data, refetch } = useQuery(gql`
         query items($page: Int!) {
             items(page: $page) {
                 pages
                 results {
                     _id
                     result
                 }
             }
         }
     `, {
         pollInterval: 5000
     });
     
+    const onClick = page => {
+        refetch({ variables: { page } });
+    };
     
     return (
         <>
             <table>
                 {data.items.results.map(({ _id, result }) => (
                     <tr key={_id}>
                         <td>{result}</td>
                     </tr>
                 ))}
             </table>
+            {_.chain(data.items.pages)
+                .map(page => (
+                    <Button onClick={() => onClick(page)}>
+                        Page {page + 1}
+                    </Button>
+                ))
+                .value()}
         </>
     );
 };

This works… for a few seconds. But then we’re unexpectedly brought back to the first page. What’s happening here?

It turns out that our polling query will always query the server with the variables it was given at the time polling was initialized. So in our case, even though the user advanced to page two, our polling query will fetch page one and render those results.

So how do we deal with this? This GitHub issue on the apollo-client project suggests calling stopPolling before changing the query’s variables, and startPolling to re-enable polling with those new variables.

In our case, that would look something like this:


 const Table = () => {
-    let { data, refetch } = useQuery(gql`
+    let { data, refetch, startPolling, stopPolling } = useQuery(gql`
         query items($page: Int!) {
             items(page: $page) {
                 pages
                 results {
                     _id
                     result
                 }
             }
         }
     `, {
         pollInterval: 5000
     });
     
     const onClick = page => {
+        stopPolling();
         refetch({ variables: { page } });
+        startPolling(5000);
     };
     
     return (
         <>
             <table>
                 {data.items.results.map(({ _id, result }) => (
                     <tr key={_id}>
                         <td>{result}</td>
                     </tr>
                 ))}
             </table>
             {_.chain(data.items.pages)
                 .map(page => (
                 <Button onClick={() => onClick(page)}>
                     Page {page + 1}
                 </Button>
                 ))
                 .value()}
         </>
     );
 };

And it works! Now our polling queries will fetch from the server with the correctly updated variables. When a user navigates to page two, they’ll stay on page two!

My best guess for why this is happening, and why the stopPolling/startPolling solution works is that when polling is started, the value of variables is trapped in a closure. When refetch is called, it changes the reference to the options.variables object, but not the referenced object. This means the value of options.variables doesn’t change within polling interval.

Calling stopPolling and startPolling forces our polling interval to restart under a new closure with our new variables values.