Skip to content

Commit 5e36d3a

Browse files
authored
add api.util.selectInvalidatedBy (#1665)
1 parent 0a16fb5 commit 5e36d3a

File tree

5 files changed

+125
-36
lines changed

5 files changed

+125
-36
lines changed

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

Lines changed: 11 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { isAnyOf, isFulfilled, isRejectedWithValue } from '@reduxjs/toolkit'
22

33
import type { FullTagDescription } from '../../endpointDefinitions'
44
import { calculateProvidedBy } from '../../endpointDefinitions'
5-
import { flatten } from '../../utils'
65
import type { QueryCacheKey } from '../apiState'
76
import { QueryStatus } from '../apiState'
87
import { calculateProvidedByThunk } from '../buildThunks'
@@ -59,39 +58,27 @@ export const build: SubMiddlewareBuilder = ({
5958

6059
function invalidateTags(
6160
tags: readonly FullTagDescription<string>[],
62-
api: SubMiddlewareApi
61+
mwApi: SubMiddlewareApi
6362
) {
64-
const state = api.getState()[reducerPath]
63+
const rootState = mwApi.getState()
64+
const state = rootState[reducerPath]
6565

66-
const toInvalidate = new Set<QueryCacheKey>()
67-
for (const tag of tags) {
68-
const provided = state.provided[tag.type]
69-
if (!provided) {
70-
continue
71-
}
72-
73-
let invalidateSubscriptions =
74-
(tag.id !== undefined
75-
? // id given: invalidate all queries that provide this type & id
76-
provided[tag.id]
77-
: // no id: invalidate all queries that provide this type
78-
flatten(Object.values(provided))) ?? []
79-
80-
for (const invalidate of invalidateSubscriptions) {
81-
toInvalidate.add(invalidate)
82-
}
83-
}
66+
const toInvalidate = api.util.selectInvalidatedBy(rootState, tags)
8467

8568
context.batch(() => {
8669
const valuesArray = Array.from(toInvalidate.values())
87-
for (const queryCacheKey of valuesArray) {
70+
for (const { queryCacheKey } of valuesArray) {
8871
const querySubState = state.queries[queryCacheKey]
8972
const subscriptionSubState = state.subscriptions[queryCacheKey]
9073
if (querySubState && subscriptionSubState) {
9174
if (Object.keys(subscriptionSubState).length === 0) {
92-
api.dispatch(removeQueryResult({ queryCacheKey }))
75+
mwApi.dispatch(
76+
removeQueryResult({
77+
queryCacheKey: queryCacheKey as QueryCacheKey,
78+
})
79+
)
9380
} else if (querySubState.status !== QueryStatus.uninitialized) {
94-
api.dispatch(refetchQuery(querySubState, queryCacheKey))
81+
mwApi.dispatch(refetchQuery(querySubState, queryCacheKey))
9582
} else {
9683
}
9784
}

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

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type {
44
QuerySubState,
55
RootState as _RootState,
66
RequestStatusFlags,
7+
QueryCacheKey,
78
} from './apiState'
89
import { QueryStatus, getRequestStatusFlags } from './apiState'
910
import type {
@@ -13,9 +14,12 @@ import type {
1314
QueryArgFrom,
1415
TagTypesFrom,
1516
ReducerPathFrom,
17+
TagDescription,
1618
} from '../endpointDefinitions'
19+
import { expandTagDescription } from '../endpointDefinitions'
1720
import type { InternalSerializeQueryArgs } from '../defaultSerializeQueryArgs'
1821
import { getMutationCacheKey } from './buildSlice'
22+
import { flatten } from '../utils'
1923

2024
export type SkipToken = typeof skipToken
2125
/**
@@ -125,7 +129,7 @@ export function buildSelectors<
125129
}) {
126130
type RootState = _RootState<Definitions, string, string>
127131

128-
return { buildQuerySelector, buildMutationSelector }
132+
return { buildQuerySelector, buildMutationSelector, selectInvalidatedBy }
129133

130134
function withRequestFlags<T extends { status: QueryStatus }>(
131135
substate: T
@@ -193,4 +197,48 @@ export function buildSelectors<
193197
return createSelector(selectMutationSubstate, withRequestFlags)
194198
}
195199
}
200+
201+
function selectInvalidatedBy(
202+
state: RootState,
203+
tags: ReadonlyArray<TagDescription<string>>
204+
): Array<{
205+
endpointName: string
206+
originalArgs: any
207+
queryCacheKey: QueryCacheKey
208+
}> {
209+
const apiState = state[reducerPath]
210+
const toInvalidate = new Set<QueryCacheKey>()
211+
for (const tag of tags.map(expandTagDescription)) {
212+
const provided = apiState.provided[tag.type]
213+
if (!provided) {
214+
continue
215+
}
216+
217+
let invalidateSubscriptions =
218+
(tag.id !== undefined
219+
? // id given: invalidate all queries that provide this type & id
220+
provided[tag.id]
221+
: // no id: invalidate all queries that provide this type
222+
flatten(Object.values(provided))) ?? []
223+
224+
for (const invalidate of invalidateSubscriptions) {
225+
toInvalidate.add(invalidate)
226+
}
227+
}
228+
229+
return flatten(
230+
Array.from(toInvalidate.values()).map((queryCacheKey) => {
231+
const querySubState = apiState.queries[queryCacheKey]
232+
return querySubState
233+
? [
234+
{
235+
queryCacheKey,
236+
endpointName: querySubState.endpointName!,
237+
originalArgs: querySubState.originalArgs,
238+
},
239+
]
240+
: []
241+
})
242+
)
243+
}
196244
}

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

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import type {
1717
QueryDefinition,
1818
MutationDefinition,
1919
AssertTagTypes,
20-
FullTagDescription,
20+
TagDescription,
2121
} from '../endpointDefinitions'
2222
import { isQueryDefinition, isMutationDefinition } from '../endpointDefinitions'
2323
import type {
@@ -274,9 +274,18 @@ declare module '../apiTypes' {
274274
* ```
275275
*/
276276
invalidateTags: ActionCreatorWithPayload<
277-
Array<TagTypes | FullTagDescription<TagTypes>>,
277+
Array<TagDescription<TagTypes>>,
278278
string
279279
>
280+
281+
selectInvalidatedBy: (
282+
state: RootState<Definitions, string, ReducerPath>,
283+
tags: ReadonlyArray<TagDescription<TagTypes>>
284+
) => Array<{
285+
endpointName: string
286+
originalArgs: any
287+
queryCacheKey: string
288+
}>
280289
}
281290
/**
282291
* Endpoints based on the input endpoints provided to `createApi`, containing `select` and `action matchers`.
@@ -463,10 +472,13 @@ export const coreModule = (): Module<CoreModule> => ({
463472

464473
safeAssign(api, { reducer: reducer as any, middleware })
465474

466-
const { buildQuerySelector, buildMutationSelector } = buildSelectors({
467-
serializeQueryArgs: serializeQueryArgs as any,
468-
reducerPath,
469-
})
475+
const { buildQuerySelector, buildMutationSelector, selectInvalidatedBy } =
476+
buildSelectors({
477+
serializeQueryArgs: serializeQueryArgs as any,
478+
reducerPath,
479+
})
480+
481+
safeAssign(api.util, { selectInvalidatedBy })
470482

471483
const {
472484
buildInitiateQuery,

packages/toolkit/src/query/endpointDefinitions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -453,7 +453,7 @@ function isFunction<T>(t: T): t is Extract<T, Function> {
453453
return typeof t === 'function'
454454
}
455455

456-
function expandTagDescription(
456+
export function expandTagDescription(
457457
description: TagDescription<string>
458458
): FullTagDescription<string> {
459459
return typeof description === 'string' ? { type: description } : description

packages/toolkit/src/query/tests/invalidation.test.tsx

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
11
import { createApi, fakeBaseQuery } from '@reduxjs/toolkit/query'
22
import { setupApiStore, waitMs } from './helpers'
3-
import type { ResultDescription } from '@reduxjs/toolkit/dist/query/endpointDefinitions'
3+
import type { TagDescription } from '@reduxjs/toolkit/dist/query/endpointDefinitions'
4+
import { waitFor } from '@testing-library/react'
45

5-
const tagTypes = ['apple', 'pear', 'banana', 'tomato'] as const
6+
const tagTypes = [
7+
'apple',
8+
'pear',
9+
'banana',
10+
'tomato',
11+
'cat',
12+
'dog',
13+
'giraffe',
14+
] as const
615
type TagTypes = typeof tagTypes[number]
7-
type Tags = ResultDescription<TagTypes, any, any, any>
16+
type Tags = TagDescription<TagTypes>[]
817

918
/** providesTags, invalidatesTags, shouldInvalidate */
1019
const caseMatrix: [Tags, Tags, boolean][] = [
@@ -62,8 +71,9 @@ test.each(caseMatrix)(
6271
let queryCount = 0
6372
const {
6473
store,
74+
api,
6575
api: {
66-
endpoints: { invalidating, providing },
76+
endpoints: { invalidating, providing, unrelated },
6777
},
6878
} = setupApiStore(
6979
createApi({
@@ -77,6 +87,12 @@ test.each(caseMatrix)(
7787
},
7888
providesTags,
7989
}),
90+
unrelated: build.query<unknown, void>({
91+
queryFn() {
92+
return { data: {} }
93+
},
94+
providesTags: ['cat', 'dog', { type: 'giraffe', id: 8 }],
95+
}),
8096
invalidating: build.mutation<unknown, void>({
8197
queryFn() {
8298
return { data: {} }
@@ -88,7 +104,33 @@ test.each(caseMatrix)(
88104
)
89105

90106
store.dispatch(providing.initiate())
107+
store.dispatch(unrelated.initiate())
91108
expect(queryCount).toBe(1)
109+
await waitFor(() => {
110+
expect(api.endpoints.providing.select()(store.getState()).status).toBe(
111+
'fulfilled'
112+
)
113+
expect(api.endpoints.unrelated.select()(store.getState()).status).toBe(
114+
'fulfilled'
115+
)
116+
})
117+
const toInvalidate = api.util.selectInvalidatedBy(
118+
store.getState(),
119+
invalidatesTags
120+
)
121+
122+
if (shouldInvalidate) {
123+
expect(toInvalidate).toEqual([
124+
{
125+
queryCacheKey: 'providing(undefined)',
126+
endpointName: 'providing',
127+
originalArgs: undefined,
128+
},
129+
])
130+
} else {
131+
expect(toInvalidate).toEqual([])
132+
}
133+
92134
store.dispatch(invalidating.initiate())
93135
expect(queryCount).toBe(1)
94136
await waitMs(2)

0 commit comments

Comments
 (0)