diff --git a/packages/toolkit/src/query/core/buildMiddleware/index.ts b/packages/toolkit/src/query/core/buildMiddleware/index.ts index ecffe44b45..1f55b5ef22 100644 --- a/packages/toolkit/src/query/core/buildMiddleware/index.ts +++ b/packages/toolkit/src/query/core/buildMiddleware/index.ts @@ -50,7 +50,7 @@ export function buildMiddleware< const actions = { invalidateTags: createAction< - Array> + Array | null | undefined> >(`${reducerPath}/invalidateTags`), } diff --git a/packages/toolkit/src/query/core/buildSelectors.ts b/packages/toolkit/src/query/core/buildSelectors.ts index 2ac4893422..c20db23950 100644 --- a/packages/toolkit/src/query/core/buildSelectors.ts +++ b/packages/toolkit/src/query/core/buildSelectors.ts @@ -9,7 +9,7 @@ import type { TagTypesFrom, } from '../endpointDefinitions' import { expandTagDescription } from '../endpointDefinitions' -import { flatten } from '../utils' +import { flatten, isNotNullish } from '../utils' import type { MutationSubState, QueryCacheKey, @@ -206,7 +206,7 @@ export function buildSelectors< function selectInvalidatedBy( state: RootState, - tags: ReadonlyArray>, + tags: ReadonlyArray | null | undefined>, ): Array<{ endpointName: string originalArgs: any @@ -214,7 +214,7 @@ export function buildSelectors< }> { const apiState = state[reducerPath] const toInvalidate = new Set() - for (const tag of tags.map(expandTagDescription)) { + for (const tag of tags.filter(isNotNullish).map(expandTagDescription)) { const provided = apiState.provided[tag.type] if (!provided) { continue diff --git a/packages/toolkit/src/query/core/module.ts b/packages/toolkit/src/query/core/module.ts index 074aa08eb8..6bb43bc411 100644 --- a/packages/toolkit/src/query/core/module.ts +++ b/packages/toolkit/src/query/core/module.ts @@ -350,7 +350,7 @@ export interface ApiModules< * ``` */ invalidateTags: ActionCreatorWithPayload< - Array>, + Array | null | undefined>, string > @@ -361,7 +361,7 @@ export interface ApiModules< */ selectInvalidatedBy: ( state: RootState, - tags: ReadonlyArray>, + tags: ReadonlyArray | null | undefined>, ) => Array<{ endpointName: string originalArgs: any diff --git a/packages/toolkit/src/query/endpointDefinitions.ts b/packages/toolkit/src/query/endpointDefinitions.ts index 38e5343475..8f7955468c 100644 --- a/packages/toolkit/src/query/endpointDefinitions.ts +++ b/packages/toolkit/src/query/endpointDefinitions.ts @@ -29,6 +29,7 @@ import type { OmitFromUnion, UnwrapPromise, } from './tsHelpers' +import { isNotNullish } from './utils' const resultType = /* @__PURE__ */ Symbol() const baseQuery = /* @__PURE__ */ Symbol() @@ -224,7 +225,7 @@ export type GetResultDescriptionFn< error: ErrorType | undefined, arg: QueryArg, meta: MetaType, -) => ReadonlyArray> +) => ReadonlyArray | undefined | null> export type FullTagDescription = { type: TagType @@ -242,7 +243,7 @@ export type ResultDescription< ErrorType, MetaType, > = - | ReadonlyArray> + | ReadonlyArray | undefined | null> | GetResultDescriptionFn type QueryTypes< @@ -778,6 +779,7 @@ export function calculateProvidedBy( queryArg, meta as MetaType, ) + .filter(isNotNullish) .map(expandTagDescription) .map(assertTagTypes) } diff --git a/packages/toolkit/src/query/tests/buildMiddleware.test.tsx b/packages/toolkit/src/query/tests/buildMiddleware.test.tsx index 0e52cb0cd1..0a03396302 100644 --- a/packages/toolkit/src/query/tests/buildMiddleware.test.tsx +++ b/packages/toolkit/src/query/tests/buildMiddleware.test.tsx @@ -1,6 +1,6 @@ import { createApi } from '@reduxjs/toolkit/query' -import { actionsReducer, setupApiStore } from '../../tests/utils/helpers' import { delay } from 'msw' +import { actionsReducer, setupApiStore } from '../../tests/utils/helpers' const baseQuery = (args?: any) => ({ data: args }) const api = createApi({ @@ -25,9 +25,15 @@ const api = createApi({ }, providesTags: ['Bread'], }), + invalidateFruit: build.mutation({ + query: (fruit?: 'Banana' | 'Bread' | null) => ({ url: `invalidate/fruit/${fruit || ''}` }), + invalidatesTags(result, error, arg) { + return [arg] + } + }) }), }) -const { getBanana, getBread } = api.endpoints +const { getBanana, getBread, invalidateFruit } = api.endpoints const storeRef = setupApiStore(api, { ...actionsReducer, @@ -70,3 +76,61 @@ it('invalidates the specified tags', async () => { getBread.matchFulfilled, ) }) + +it('invalidates tags correctly when null or undefined are provided as tags', async() =>{ + await storeRef.store.dispatch(getBanana.initiate(1)) + await storeRef.store.dispatch(api.util.invalidateTags([undefined, null, 'Banana'])) + + // Slight pause to let the middleware run and such + await delay(20) + + const apiActions = [ + api.internalActions.middlewareRegistered.match, + getBanana.matchPending, + getBanana.matchFulfilled, + api.util.invalidateTags.match, + getBanana.matchPending, + getBanana.matchFulfilled, + ] + + expect(storeRef.store.getState().actions).toMatchSequence(...apiActions) +}) + + +it.each([ + { tags: [undefined, null, 'Bread'] as Parameters['0'] }, + { tags: [undefined, null], }, { tags: [] }] +)('does not invalidate with tags=$tags if no query matches', async ({ tags }) => { + await storeRef.store.dispatch(getBanana.initiate(1)) + await storeRef.store.dispatch(api.util.invalidateTags(tags)) + + // Slight pause to let the middleware run and such + await delay(20) + + const apiActions = [ + api.internalActions.middlewareRegistered.match, + getBanana.matchPending, + getBanana.matchFulfilled, + api.util.invalidateTags.match, + ] + + expect(storeRef.store.getState().actions).toMatchSequence(...apiActions) +}) + +it.each([{ mutationArg: 'Bread' as "Bread" | null | undefined }, { mutationArg: undefined }, { mutationArg: null }])('does not invalidate queries when a mutation with tags=[$mutationArg] runs and does not match anything', async ({ mutationArg }) => { + await storeRef.store.dispatch(getBanana.initiate(1)) + await storeRef.store.dispatch(invalidateFruit.initiate(mutationArg)) + + // Slight pause to let the middleware run and such + await delay(20) + + const apiActions = [ + api.internalActions.middlewareRegistered.match, + getBanana.matchPending, + getBanana.matchFulfilled, + invalidateFruit.matchPending, + invalidateFruit.matchFulfilled, + ] + + expect(storeRef.store.getState().actions).toMatchSequence(...apiActions) +}) \ No newline at end of file diff --git a/packages/toolkit/src/query/tests/createApi.test-d.ts b/packages/toolkit/src/query/tests/createApi.test-d.ts index b33ced1f6e..2b1dc64d9e 100644 --- a/packages/toolkit/src/query/tests/createApi.test-d.ts +++ b/packages/toolkit/src/query/tests/createApi.test-d.ts @@ -39,7 +39,7 @@ describe('type tests', () => { expectTypeOf(api.util.invalidateTags) .parameter(0) - .toEqualTypeOf[]>() + .toEqualTypeOf<(null | undefined | TagDescription)[]>() }) describe('endpoint definition typings', () => { diff --git a/packages/toolkit/src/query/tests/invalidation.test.tsx b/packages/toolkit/src/query/tests/invalidation.test.tsx index 09b71cdee2..03a7638663 100644 --- a/packages/toolkit/src/query/tests/invalidation.test.tsx +++ b/packages/toolkit/src/query/tests/invalidation.test.tsx @@ -14,10 +14,10 @@ const tagTypes = [ 'giraffe', ] as const type TagTypes = (typeof tagTypes)[number] -type Tags = TagDescription[] - +type ProvidedTags = TagDescription[] +type InvalidatesTags = (ProvidedTags[number] | null | undefined)[] /** providesTags, invalidatesTags, shouldInvalidate */ -const caseMatrix: [Tags, Tags, boolean][] = [ +const caseMatrix: [ProvidedTags, InvalidatesTags, boolean][] = [ // ***************************** // basic invalidation behavior // ***************************** @@ -39,7 +39,11 @@ const caseMatrix: [Tags, Tags, boolean][] = [ // type + id invalidates type + id [[{ type: 'apple', id: 1 }], [{ type: 'apple', id: 1 }], true], [[{ type: 'apple', id: 1 }], [{ type: 'apple', id: 2 }], false], - + // null and undefined + [['apple'], [null], false], + [['apple'], [undefined], false], + [['apple'], [null, 'apple'], true], + [['apple'], [undefined, 'apple'], true], // ***************************** // test multiple values in array // *****************************