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.