Skip to content

Commit

Permalink
feat: Add shouldRefetchOnPolling option to control polling refetch be…
Browse files Browse the repository at this point in the history
…havior
  • Loading branch information
aditya-kumawat committed Dec 6, 2023
1 parent aaf990b commit 7e9f466
Show file tree
Hide file tree
Showing 4 changed files with 219 additions and 1 deletion.
14 changes: 14 additions & 0 deletions .changeset/swift-zoos-collect.md
@@ -0,0 +1,14 @@
Adds a new `skipPollAttempt` callback function that's called whenever a refetch attempt occurs while polling. If the function returns `true`, the refetch is skipped and not reattempted until the next poll interval. This will solve the frequent use-case of disabling polling when the window is inactive.
```ts
useQuery(QUERY, {
pollInterval: 1000,
skipPollAttempt: () => document.hidden // or !document.hasFocus()
});
// or define it globally
new ApolloClient({
defaultOptions: {
watchQuery: {
skipPollAttempt: () => document.hidden // or !document.hasFocus()
}
}
})
5 changes: 4 additions & 1 deletion src/core/ObservableQuery.ts
Expand Up @@ -776,7 +776,10 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`,

const maybeFetch = () => {
if (this.pollingInfo) {
if (!isNetworkRequestInFlight(this.queryInfo.networkStatus)) {
if (
!isNetworkRequestInFlight(this.queryInfo.networkStatus) &&
!this.options.skipPollAttempt?.()
) {
this.reobserve(
{
// Most fetchPolicy options don't make sense to use in a polling context, as
Expand Down
7 changes: 7 additions & 0 deletions src/core/watchQueryOptions.ts
Expand Up @@ -176,6 +176,13 @@ export interface WatchQueryOptions<

/** {@inheritDoc @apollo/client!QueryOptions#canonizeResults:member} */
canonizeResults?: boolean;

/**
* A callback function that's called whenever a refetch attempt occurs
* while polling. If the function returns `true`, the refetch is
* skipped and not reattempted until the next poll interval.
*/
skipPollAttempt?: () => boolean;
}

export interface NextFetchPolicyContext<
Expand Down
194 changes: 194 additions & 0 deletions src/react/hooks/__tests__/useQuery.test.tsx
Expand Up @@ -2092,6 +2092,200 @@ describe("useQuery Hook", () => {
unmount();
result.current.stopPolling();
});

describe("should prevent fetches when `skipPollAttempt` returns `false`", () => {
beforeEach(() => {
jest.useFakeTimers();
});

afterEach(() => {
jest.runOnlyPendingTimers();
jest.useRealTimers();
});

it("when defined as a global default option", async () => {
const skipPollAttempt = jest.fn().mockImplementation(() => false);

const query = gql`
{
hello
}
`;
const link = mockSingleLink(
{
request: { query },
result: { data: { hello: "world 1" } },
},
{
request: { query },
result: { data: { hello: "world 2" } },
},
{
request: { query },
result: { data: { hello: "world 3" } },
}
);

const client = new ApolloClient({
link,
cache: new InMemoryCache(),
defaultOptions: {
watchQuery: {
skipPollAttempt,
},
},
});

const wrapper = ({ children }: any) => (
<ApolloProvider client={client}>{children}</ApolloProvider>
);

const { result } = renderHook(
() => useQuery(query, { pollInterval: 10 }),
{ wrapper }
);

expect(result.current.loading).toBe(true);
expect(result.current.data).toBe(undefined);

await waitFor(
() => {
expect(result.current.data).toEqual({ hello: "world 1" });
},
{ interval: 1 }
);

expect(result.current.loading).toBe(false);

await waitFor(
() => {
expect(result.current.data).toEqual({ hello: "world 2" });
},
{ interval: 1 }
);

skipPollAttempt.mockImplementation(() => true);
expect(result.current.loading).toBe(false);

await jest.advanceTimersByTime(12);
await waitFor(
() => expect(result.current.data).toEqual({ hello: "world 2" }),
{ interval: 1 }
);

await jest.advanceTimersByTime(12);
await waitFor(
() => expect(result.current.data).toEqual({ hello: "world 2" }),
{ interval: 1 }
);

await jest.advanceTimersByTime(12);
await waitFor(
() => expect(result.current.data).toEqual({ hello: "world 2" }),
{ interval: 1 }
);

skipPollAttempt.mockImplementation(() => false);
expect(result.current.loading).toBe(false);

await waitFor(
() => {
expect(result.current.data).toEqual({ hello: "world 3" });
},
{ interval: 1 }
);
});

it("when defined for a single query", async () => {
const skipPollAttempt = jest.fn().mockImplementation(() => false);

const query = gql`
{
hello
}
`;
const mocks = [
{
request: { query },
result: { data: { hello: "world 1" } },
},
{
request: { query },
result: { data: { hello: "world 2" } },
},
{
request: { query },
result: { data: { hello: "world 3" } },
},
];

const cache = new InMemoryCache();
const wrapper = ({ children }: any) => (
<MockedProvider mocks={mocks} cache={cache}>
{children}
</MockedProvider>
);

const { result } = renderHook(
() =>
useQuery(query, {
pollInterval: 10,
skipPollAttempt,
}),
{ wrapper }
);

expect(result.current.loading).toBe(true);
expect(result.current.data).toBe(undefined);

await waitFor(
() => {
expect(result.current.data).toEqual({ hello: "world 1" });
},
{ interval: 1 }
);

expect(result.current.loading).toBe(false);

await waitFor(
() => {
expect(result.current.data).toEqual({ hello: "world 2" });
},
{ interval: 1 }
);

skipPollAttempt.mockImplementation(() => true);
expect(result.current.loading).toBe(false);

await jest.advanceTimersByTime(12);
await waitFor(
() => expect(result.current.data).toEqual({ hello: "world 2" }),
{ interval: 1 }
);

await jest.advanceTimersByTime(12);
await waitFor(
() => expect(result.current.data).toEqual({ hello: "world 2" }),
{ interval: 1 }
);

await jest.advanceTimersByTime(12);
await waitFor(
() => expect(result.current.data).toEqual({ hello: "world 2" }),
{ interval: 1 }
);

skipPollAttempt.mockImplementation(() => false);
expect(result.current.loading).toBe(false);

await waitFor(
() => {
expect(result.current.data).toEqual({ hello: "world 3" });
},
{ interval: 1 }
);
});
});
});

describe("Error handling", () => {
Expand Down

0 comments on commit 7e9f466

Please sign in to comment.