Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Option for queries to pause polling when unfocused #4055

Merged
merged 11 commits into from
Jan 24, 2024
8 changes: 8 additions & 0 deletions packages/toolkit/src/query/core/apiState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,14 @@ export type SubscriptionOptions = {
* How frequently to automatically re-fetch data (in milliseconds). Defaults to `0` (off).
*/
pollingInterval?: number
/**
* Defaults to 'false'. This setting allows you to control whether RTK Query will continue polling if the window is not focused.
*
* If pollingInterval is not set or set to 0, this **will not be evaluated** until pollingInterval is greater than 0.
*
* Note: requires [`setupListeners`](./setupListeners) to have been called.
*/
skipPollingIfUnfocused?: boolean
/**
* Defaults to `false`. This setting allows you to control whether RTK Query will try to refetch all subscribed queries after regaining a network connection.
*
Expand Down
26 changes: 17 additions & 9 deletions packages/toolkit/src/query/core/buildMiddleware/polling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ export const buildPollingHandler: InternalHandlerBuilder = ({
if (!querySubState || querySubState.status === QueryStatus.uninitialized)
return

const lowestPollingInterval = findLowestPollingInterval(subscriptions)
const { lowestPollingInterval, skipPollingIfUnfocused } =
findLowestPollingInterval(subscriptions)
if (!Number.isFinite(lowestPollingInterval)) return

const currentPoll = currentPolls[queryCacheKey]
Expand All @@ -72,16 +73,16 @@ export const buildPollingHandler: InternalHandlerBuilder = ({

const nextPollTimestamp = Date.now() + lowestPollingInterval

const currentInterval: typeof currentPolls[number] = (currentPolls[
queryCacheKey
] = {
currentPolls[queryCacheKey] = {
nextPollTimestamp,
pollingInterval: lowestPollingInterval,
timeout: setTimeout(() => {
currentInterval!.timeout = undefined
api.dispatch(refetchQuery(querySubState, queryCacheKey))
if (state.config.focused || !skipPollingIfUnfocused) {
api.dispatch(refetchQuery(querySubState, queryCacheKey))
}
startNextPoll({ queryCacheKey }, api)
}, lowestPollingInterval),
})
}
}

function updatePollingInterval(
Expand All @@ -96,7 +97,7 @@ export const buildPollingHandler: InternalHandlerBuilder = ({
return
}

const lowestPollingInterval = findLowestPollingInterval(subscriptions)
const { lowestPollingInterval } = findLowestPollingInterval(subscriptions)

if (!Number.isFinite(lowestPollingInterval)) {
cleanupPollForKey(queryCacheKey)
Expand Down Expand Up @@ -126,17 +127,24 @@ export const buildPollingHandler: InternalHandlerBuilder = ({
}

function findLowestPollingInterval(subscribers: Subscribers = {}) {
let skipPollingIfUnfocused: boolean | undefined = false
let lowestPollingInterval = Number.POSITIVE_INFINITY
for (let key in subscribers) {
if (!!subscribers[key].pollingInterval) {
lowestPollingInterval = Math.min(
subscribers[key].pollingInterval!,
lowestPollingInterval
)
skipPollingIfUnfocused =
subscribers[key].skipPollingIfUnfocused || skipPollingIfUnfocused
}
}

return lowestPollingInterval
return {
lowestPollingInterval,
skipPollingIfUnfocused,
}
}

return handler
}
4 changes: 4 additions & 0 deletions packages/toolkit/src/query/react/buildHooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,7 @@ export function buildHooks<Definitions extends EndpointDefinitions>({
refetchOnMountOrArgChange,
skip = false,
pollingInterval = 0,
skipPollingIfUnfocused = false,
} = {}
) => {
const { initiate } = api.endpoints[name] as ApiEndpointQuery<
Expand Down Expand Up @@ -715,6 +716,7 @@ export function buildHooks<Definitions extends EndpointDefinitions>({
refetchOnReconnect,
refetchOnFocus,
pollingInterval,
skipPollingIfUnfocused,
})

const lastRenderHadSubscription = useRef(false)
Expand Down Expand Up @@ -815,6 +817,7 @@ export function buildHooks<Definitions extends EndpointDefinitions>({
refetchOnReconnect,
refetchOnFocus,
pollingInterval = 0,
skipPollingIfUnfocused = false,
} = {}) => {
const { initiate } = api.endpoints[name] as ApiEndpointQuery<
QueryDefinition<any, any, any, any, any>,
Expand All @@ -829,6 +832,7 @@ export function buildHooks<Definitions extends EndpointDefinitions>({
refetchOnReconnect,
refetchOnFocus,
pollingInterval,
skipPollingIfUnfocused,
})

usePossiblyImmediateEffect(() => {
Expand Down
108 changes: 108 additions & 0 deletions packages/toolkit/src/query/tests/polling.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,4 +122,112 @@ describe('polling tests', () => {

expect(mockBaseQuery.mock.calls.length).toBeGreaterThanOrEqual(2)
})

it('respects skipPollingIfUnfocused', async () => {
mockBaseQuery.mockClear()
storeRef.store.dispatch(
getPosts.initiate(2, {
subscriptionOptions: {
pollingInterval: 10,
skipPollingIfUnfocused: true,
},
subscribe: true,
})
)
storeRef.store.dispatch(api.internalActions?.onFocusLost())

await delay(50)
const callsWithSkip = mockBaseQuery.mock.calls.length

storeRef.store.dispatch(
getPosts.initiate(2, {
subscriptionOptions: {
pollingInterval: 10,
skipPollingIfUnfocused: false,
},
subscribe: true,
})
)

storeRef.store.dispatch(api.internalActions?.onFocus())

await delay(50)
const callsWithoutSkip = mockBaseQuery.mock.calls.length

expect(callsWithSkip).toBe(1)
expect(callsWithoutSkip).toBeGreaterThan(2)

storeRef.store.dispatch(api.util.resetApiState())
})

it('respects skipPollingIfUnfocused if at least one subscription has it', async () => {
storeRef.store.dispatch(
getPosts.initiate(3, {
subscriptionOptions: {
pollingInterval: 10,
skipPollingIfUnfocused: false,
},
subscribe: true,
})
)

await delay(50)
const callsWithoutSkip = mockBaseQuery.mock.calls.length

storeRef.store.dispatch(
getPosts.initiate(3, {
subscriptionOptions: {
pollingInterval: 15,
skipPollingIfUnfocused: true,
},
subscribe: true,
})
)

storeRef.store.dispatch(
getPosts.initiate(3, {
subscriptionOptions: {
pollingInterval: 20,
skipPollingIfUnfocused: false,
},
subscribe: true,
})
)

storeRef.store.dispatch(api.internalActions?.onFocusLost())

await delay(50)
const callsWithSkip = mockBaseQuery.mock.calls.length

expect(callsWithoutSkip).toBeGreaterThan(2)
expect(callsWithSkip).toBe(callsWithoutSkip + 1)
})

it('replaces skipPollingIfUnfocused when the subscription options are updated', async () => {
const { requestId, queryCacheKey, ...subscription } =
storeRef.store.dispatch(
getPosts.initiate(1, {
subscriptionOptions: {
pollingInterval: 10,
skipPollingIfUnfocused: false,
},
subscribe: true,
})
)

const getSubs = createSubscriptionGetter(queryCacheKey)

await delay(1)
expect(Object.keys(getSubs())).toHaveLength(1)
expect(getSubs()[requestId].skipPollingIfUnfocused).toBe(false)

subscription.updateSubscriptionOptions({
pollingInterval: 20,
skipPollingIfUnfocused: true,
})

await delay(1)
expect(Object.keys(getSubs())).toHaveLength(1)
expect(getSubs()[requestId].skipPollingIfUnfocused).toBe(true)
})
})