Skip to content

Commit f994418

Browse files
committed
feat(rtk-query): unsubscribe prefetched queries after configurable timer #1283
Closes #1283 Automatically removes prefetch subscriptions after configurable amount of time. Description: Prefetch subscription are now removed after `prefetchOptions.keepSubscriptionFor` if provided or api.config.keepPrefetchSubscriptionsFor otherwise. Api changes: - adds `keepSubscriptionFor` to prefetchOptions - adds `keepPrefetchSubscriptionsFor` to api.config (default 10s) Internal changes: - prefetch queries now have the same requestId and the same subscription key
1 parent bc444ff commit f994418

File tree

11 files changed

+271
-13
lines changed

11 files changed

+271
-13
lines changed

examples/query/react/prefetching/src/features/posts/PostsManager.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ const getColorForStatus = (status: Post['status']) => {
3030
const PostList = () => {
3131
const [page, setPage] = useState(1)
3232
const { data: posts, isLoading, isFetching } = useListPostsQuery(page)
33-
const prefetchPage = usePrefetch('listPosts')
33+
const prefetchPage = usePrefetch('listPosts', { keepSubscriptionFor: 5 })
3434

3535
const prefetchNext = useCallback(() => {
3636
prefetchPage(page + 1)

packages/toolkit/src/query/apiTypes.ts

+1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export type Module<Name extends ModuleName> = {
4242
| 'reducerPath'
4343
| 'serializeQueryArgs'
4444
| 'keepUnusedDataFor'
45+
| 'keepPrefetchSubscriptionsFor'
4546
| 'refetchOnMountOrArgChange'
4647
| 'refetchOnFocus'
4748
| 'refetchOnReconnect'

packages/toolkit/src/query/core/apiState.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,8 @@ export type ConfigState<ReducerPath> = RefetchConfigOptions & {
253253
} & ModifiableConfigState
254254

255255
export type ModifiableConfigState = {
256-
keepUnusedDataFor: number
256+
keepUnusedDataFor: number,
257+
keepPrefetchSubscriptionsFor: number
257258
} & RefetchConfigOptions
258259

259260
export type MutationState<D extends EndpointDefinitions> = {

packages/toolkit/src/query/core/buildInitiate.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,15 @@ declare module './module' {
3333
}
3434
}
3535

36+
export interface PrefetchSubscribriptionOptions {
37+
keepSubscriptionFor?: number;
38+
}
39+
3640
export interface StartQueryActionCreatorOptions {
3741
subscribe?: boolean
3842
forceRefetch?: boolean | number
39-
subscriptionOptions?: SubscriptionOptions
43+
subscriptionOptions?: SubscriptionOptions,
44+
prefetch?: boolean | PrefetchSubscribriptionOptions,
4045
}
4146

4247
type StartQueryActionCreator<
@@ -258,7 +263,7 @@ Features like automatic cache collection, automatic refetching etc. will not be
258263
endpointDefinition: QueryDefinition<any, any, any, any>
259264
) {
260265
const queryAction: StartQueryActionCreator<any> =
261-
(arg, { subscribe = true, forceRefetch, subscriptionOptions } = {}) =>
266+
(arg, { subscribe = true, forceRefetch, subscriptionOptions, prefetch } = {}) =>
262267
(dispatch, getState) => {
263268
const queryCacheKey = serializeQueryArgs({
264269
queryArgs: arg,
@@ -269,12 +274,15 @@ Features like automatic cache collection, automatic refetching etc. will not be
269274
type: 'query',
270275
subscribe,
271276
forceRefetch,
277+
prefetch,
272278
subscriptionOptions,
273279
endpointName,
274280
originalArgs: arg,
275281
queryCacheKey,
282+
reducerPath: api.reducerPath,
276283
})
277284
const thunkResult = dispatch(thunk)
285+
278286
middlewareWarning(getState)
279287

280288
const { requestId, abort } = thunkResult

packages/toolkit/src/query/core/buildMiddleware/cacheCollection.ts

+50-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { BaseQueryFn } from '../../baseQueryTypes'
22
import type { QueryDefinition } from '../../endpointDefinitions'
33
import type { ConfigState, QueryCacheKey } from '../apiState'
44
import { QuerySubstateIdentifier } from '../apiState'
5+
import type { PrefetchSubscribriptionOptions } from '../buildInitiate'
56
import type {
67
QueryStateMeta,
78
SubMiddlewareApi,
@@ -28,11 +29,35 @@ declare module '../../endpointDefinitions' {
2829
}
2930
}
3031

31-
export const build: SubMiddlewareBuilder = ({ reducerPath, api, context }) => {
32+
/**
33+
* Output is in *milliseconds*.
34+
*/
35+
const getPrefetchSubscriptionTTLMs = (
36+
prefetch: true | PrefetchSubscribriptionOptions,
37+
config: ConfigState<string>
38+
): number => {
39+
if (
40+
typeof prefetch === 'object' &&
41+
prefetch !== null &&
42+
typeof prefetch.keepSubscriptionFor === 'number'
43+
) {
44+
return prefetch.keepSubscriptionFor * 1000
45+
}
46+
47+
return config.keepPrefetchSubscriptionsFor * 1000
48+
}
49+
50+
export const build: SubMiddlewareBuilder = ({
51+
reducerPath,
52+
api,
53+
context,
54+
queryThunk,
55+
}) => {
3256
const { removeQueryResult, unsubscribeQueryResult } = api.internalActions
3357

3458
return (mwApi) => {
3559
const currentRemovalTimeouts: QueryStateMeta<TimeoutId> = {}
60+
const autoUnsubscribeTimeouts: QueryStateMeta<TimeoutId> = {}
3661

3762
return (next) =>
3863
(action): any => {
@@ -50,8 +75,31 @@ export const build: SubMiddlewareBuilder = ({ reducerPath, api, context }) => {
5075
)
5176
}
5277

78+
if (queryThunk.pending.match(action) && action.meta.arg.prefetch) {
79+
const requestId = action.meta.requestId
80+
const currentTimeout = autoUnsubscribeTimeouts[requestId]
81+
82+
if (currentTimeout) {
83+
clearTimeout(currentTimeout)
84+
}
85+
86+
autoUnsubscribeTimeouts[requestId] = setTimeout(
87+
mwApi.dispatch,
88+
getPrefetchSubscriptionTTLMs(
89+
action.meta.arg.prefetch,
90+
mwApi.getState()[reducerPath].config
91+
),
92+
unsubscribeQueryResult({
93+
requestId,
94+
queryCacheKey: action.meta.arg.queryCacheKey,
95+
})
96+
)
97+
}
98+
5399
if (api.util.resetApiState.match(action)) {
54-
for (const [key, timeout] of Object.entries(currentRemovalTimeouts)) {
100+
for (const [key, timeout] of Object.entries(
101+
currentRemovalTimeouts
102+
).concat(Object.entries(autoUnsubscribeTimeouts))) {
55103
if (timeout) clearTimeout(timeout)
56104
delete currentRemovalTimeouts[key]
57105
}

packages/toolkit/src/query/core/buildMiddleware/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ export function buildMiddleware<
8080
originalArgs: querySubState.originalArgs,
8181
subscribe: false,
8282
forceRefetch: true,
83+
reducerPath: reducerPath,
8384
queryCacheKey: queryCacheKey as any,
8485
...override,
8586
})

packages/toolkit/src/query/core/buildThunks.ts

+13-4
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,14 @@ import type {
1818
ResultTypeFrom,
1919
} from '../endpointDefinitions'
2020
import { calculateProvidedBy } from '../endpointDefinitions'
21-
import type { AsyncThunkPayloadCreator, Draft } from '@reduxjs/toolkit'
2221
import {
2322
isAllOf,
2423
isFulfilled,
2524
isPending,
2625
isRejected,
2726
isRejectedWithValue,
27+
nanoid,
28+
createAsyncThunk,
2829
} from '@reduxjs/toolkit'
2930
import type { Patch } from 'immer'
3031
import { isDraftable, produceWithPatches } from 'immer'
@@ -33,9 +34,9 @@ import type {
3334
ThunkAction,
3435
ThunkDispatch,
3536
AsyncThunk,
37+
AsyncThunkPayloadCreator,
38+
Draft,
3639
} from '@reduxjs/toolkit'
37-
import { createAsyncThunk } from '@reduxjs/toolkit'
38-
3940
import { HandledError } from '../HandledError'
4041

4142
import type { ApiEndpointQuery, PrefetchOptions } from './module'
@@ -105,6 +106,7 @@ export interface QueryThunkArg
105106
type: 'query'
106107
originalArgs: unknown
107108
endpointName: string
109+
reducerPath: string
108110
}
109111

110112
export interface MutationThunkArg {
@@ -411,6 +413,13 @@ In the case of an unhandled error, no tags will be "provided" or "invalidated".`
411413

412414
return true
413415
},
416+
idGenerator(args): string {
417+
if (args.prefetch) {
418+
return `${args.reducerPath}-${args.queryCacheKey}-prefetch`
419+
}
420+
421+
return nanoid()
422+
},
414423
dispatchConditionRejection: true,
415424
})
416425

@@ -443,7 +452,7 @@ In the case of an unhandled error, no tags will be "provided" or "invalidated".`
443452
const queryAction = (force: boolean = true) =>
444453
(api.endpoints[endpointName] as ApiEndpointQuery<any, any>).initiate(
445454
arg,
446-
{ forceRefetch: force }
455+
{ forceRefetch: force, prefetch: options || true }
447456
)
448457
const latestStateValue = (
449458
api.endpoints[endpointName] as ApiEndpointQuery<any, any>

packages/toolkit/src/query/core/module.ts

+10-3
Original file line numberDiff line numberDiff line change
@@ -49,16 +49,21 @@ import { enablePatches } from 'immer'
4949
/**
5050
* `ifOlderThan` - (default: `false` | `number`) - _number is value in seconds_
5151
* - If specified, it will only run the query if the difference between `new Date()` and the last `fulfilledTimeStamp` is greater than the given value
52-
*
52+
*
53+
* - `keepSubscriptionFor`: how long before the data is considered unused;
54+
* defaults to `api.config.keepPrefetchSubscriptionsFor`. - _number is value in seconds_
55+
*
56+
*
5357
* @overloadSummary
5458
* `force`
5559
* - If `force: true`, it will ignore the `ifOlderThan` value if it is set and the query will be run even if it exists in the cache.
5660
*/
5761
export type PrefetchOptions =
5862
| {
59-
ifOlderThan?: false | number
63+
ifOlderThan?: false | number,
64+
keepSubscriptionFor?: number,
6065
}
61-
| { force?: boolean }
66+
| { force?: boolean, keepSubscriptionFor?: number, }
6267

6368
export const coreModuleName = /* @__PURE__ */ Symbol()
6469
export type CoreModule =
@@ -365,6 +370,7 @@ export const coreModule = (): Module<CoreModule> => ({
365370
reducerPath,
366371
serializeQueryArgs,
367372
keepUnusedDataFor,
373+
keepPrefetchSubscriptionsFor,
368374
refetchOnMountOrArgChange,
369375
refetchOnFocus,
370376
refetchOnReconnect,
@@ -427,6 +433,7 @@ export const coreModule = (): Module<CoreModule> => ({
427433
refetchOnReconnect,
428434
refetchOnMountOrArgChange,
429435
keepUnusedDataFor,
436+
keepPrefetchSubscriptionsFor,
430437
reducerPath,
431438
},
432439
})

packages/toolkit/src/query/createApi.ts

+31
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,36 @@ export interface CreateApiOptions<
126126
* ```
127127
*/
128128
keepUnusedDataFor?: number
129+
130+
/**
131+
* Defaults to `10` _(this value is in seconds)_.
132+
*
133+
* The default time to live of prefetch subscriptions.
134+
*
135+
* ```ts
136+
* // codeblock-meta title="keepPrefetchSubscriptionsFor example"
137+
*
138+
* import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
139+
* interface Post {
140+
* id: number
141+
* name: string
142+
* }
143+
* type PostsResponse = Post[]
144+
*
145+
* const api = createApi({
146+
* baseQuery: fetchBaseQuery({ baseUrl: '/' }),
147+
* keepPrefetchSubscriptionsFor: 5,
148+
* endpoints: (build) => ({
149+
* getPosts: build.query<PostsResponse, void>({
150+
* query: () => 'posts',
151+
* // highlight-start
152+
* // highlight-end
153+
* })
154+
* })
155+
* })
156+
* ```
157+
*/
158+
keepPrefetchSubscriptionsFor?: number
129159
/**
130160
* Defaults to `false`. This setting allows you to control whether if a cached result is already available RTK Query will only serve a cached result, or if it should `refetch` when set to `true` or if an adequate amount of time has passed since the last successful query result.
131161
* - `false` - Will not cause a query to be performed _unless_ it does not exist yet.
@@ -240,6 +270,7 @@ export function buildCreateApi<Modules extends [Module<any>, ...Module<any>[]]>(
240270
reducerPath: 'api',
241271
serializeQueryArgs: defaultSerializeQueryArgs,
242272
keepUnusedDataFor: 60,
273+
keepPrefetchSubscriptionsFor: 10,
243274
refetchOnMountOrArgChange: false,
244275
refetchOnFocus: false,
245276
refetchOnReconnect: false,

packages/toolkit/src/query/tests/buildSlice.test.ts

+1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ it('only resets the api state when resetApiState is dispatched', async () => {
4242
config: {
4343
focused: true,
4444
keepUnusedDataFor: 60,
45+
keepPrefetchSubscriptionsFor: 10,
4546
middlewareRegistered: true,
4647
online: true,
4748
reducerPath: 'api',

0 commit comments

Comments
 (0)