Skip to content

Commit

Permalink
feat: Add focusPolling option to disable refetching if tab is inactive
Browse files Browse the repository at this point in the history
  • Loading branch information
aditya-kumawat committed Nov 30, 2023
1 parent 89ec18e commit d1ea6fe
Show file tree
Hide file tree
Showing 4 changed files with 203 additions and 1 deletion.
5 changes: 5 additions & 0 deletions .changeset/swift-zoos-collect.md
@@ -0,0 +1,5 @@
---
"@apollo/client": patch
---

This adds focusPolling optional option to skip refetching if set as true. This will solve the frequent use-case of disabling polling when the window is inactive.
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.focusPolling || document?.hasFocus())
) {
this.reobserve(
{
// Most fetchPolicy options don't make sense to use in a polling context, as
Expand Down
6 changes: 6 additions & 0 deletions src/core/watchQueryOptions.ts
Expand Up @@ -176,6 +176,12 @@ export interface WatchQueryOptions<

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

/**
* Specifies if refetching of query should be enabled if window is focused, ie,
* `document.hasFocused()` returns `true`.
*/
focusPolling?: boolean;
}

export interface NextFetchPolicyContext<
Expand Down
188 changes: 188 additions & 0 deletions src/react/hooks/__tests__/useQuery.test.tsx
Expand Up @@ -1608,6 +1608,15 @@ describe("useQuery Hook", () => {
});

describe("polling", () => {
beforeEach(() => {
jest.useFakeTimers();
});

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

it("should support polling", async () => {
const query = gql`
{
Expand Down Expand Up @@ -2092,6 +2101,185 @@ describe("useQuery Hook", () => {
unmount();
result.current.stopPolling();
});

it.only("should stop refetching if `focusPolling: true` at global level", async () => {
document.hasFocus = jest.fn().mockImplementation(() => true);

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: {
focusPolling: true,
},
},
});

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 }
);

document.hasFocus = jest.fn().mockImplementation(() => false);
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 }
);

document.hasFocus = jest.fn().mockImplementation(() => true);
expect(result.current.loading).toBe(false);

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

it.only("should stop refetching if `focusPolling: true` at query level", async () => {
document.hasFocus = jest.fn().mockImplementation(() => true);

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, focusPolling: true }),
{ 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 }
);

document.hasFocus = jest.fn().mockImplementation(() => false);
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 }
);

document.hasFocus = jest.fn().mockImplementation(() => true);
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 d1ea6fe

Please sign in to comment.