From 570fb226361a3ac59725ef3db186a2739904e389 Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Tue, 31 Dec 2024 16:00:55 -0500 Subject: [PATCH] Fix updateQueryData for infinite queries (#4796) * Fix updateQueryData for infinite queries * Fix upsertQueryData for infinite queries * Fix upsertQueryEntries for infinite queries * Fix types in lifecycle middleware --- packages/toolkit/src/query/core/apiState.ts | 13 +++ .../toolkit/src/query/core/buildInitiate.ts | 2 +- .../core/buildMiddleware/cacheLifecycle.ts | 2 +- .../core/buildMiddleware/queryLifecycle.ts | 2 +- packages/toolkit/src/query/core/buildSlice.ts | 11 ++- .../toolkit/src/query/core/buildThunks.ts | 93 ++++++++++++++++--- .../src/query/tests/infiniteQueries.test.ts | 84 ++++++++++++++++- 7 files changed, 185 insertions(+), 22 deletions(-) diff --git a/packages/toolkit/src/query/core/apiState.ts b/packages/toolkit/src/query/core/apiState.ts index e068a32589..187208f824 100644 --- a/packages/toolkit/src/query/core/apiState.ts +++ b/packages/toolkit/src/query/core/apiState.ts @@ -154,6 +154,19 @@ export type QueryKeys = { ? K : never }[keyof Definitions] + +export type InfiniteQueryKeys = { + [K in keyof Definitions]: Definitions[K] extends InfiniteQueryDefinition< + any, + any, + any, + any, + any + > + ? K + : never +}[keyof Definitions] + export type MutationKeys = { [K in keyof Definitions]: Definitions[K] extends MutationDefinition< any, diff --git a/packages/toolkit/src/query/core/buildInitiate.ts b/packages/toolkit/src/query/core/buildInitiate.ts index a3727b0112..0567328ac1 100644 --- a/packages/toolkit/src/query/core/buildInitiate.ts +++ b/packages/toolkit/src/query/core/buildInitiate.ts @@ -121,7 +121,7 @@ export type QueryActionCreatorResult< export type InfiniteQueryActionCreatorResult< D extends InfiniteQueryDefinition, > = Promise> & { - arg: QueryArgFrom + arg: InfiniteQueryArgFrom requestId: string subscriptionOptions: SubscriptionOptions | undefined abort(): void diff --git a/packages/toolkit/src/query/core/buildMiddleware/cacheLifecycle.ts b/packages/toolkit/src/query/core/buildMiddleware/cacheLifecycle.ts index c13a89957d..3f2424b930 100644 --- a/packages/toolkit/src/query/core/buildMiddleware/cacheLifecycle.ts +++ b/packages/toolkit/src/query/core/buildMiddleware/cacheLifecycle.ts @@ -344,7 +344,7 @@ export const buildCacheLifecycleHandler: InternalHandlerBuilder = ({ mwApi.dispatch( api.util.updateQueryData( endpointName as never, - originalArgs, + originalArgs as never, updateRecipe, ), ) diff --git a/packages/toolkit/src/query/core/buildMiddleware/queryLifecycle.ts b/packages/toolkit/src/query/core/buildMiddleware/queryLifecycle.ts index 4746823b05..4cb55a36dc 100644 --- a/packages/toolkit/src/query/core/buildMiddleware/queryLifecycle.ts +++ b/packages/toolkit/src/query/core/buildMiddleware/queryLifecycle.ts @@ -476,7 +476,7 @@ export const buildQueryLifecycleHandler: InternalHandlerBuilder = ({ mwApi.dispatch( api.util.updateQueryData( endpointName as never, - originalArgs, + originalArgs as never, updateRecipe, ), ) diff --git a/packages/toolkit/src/query/core/buildSlice.ts b/packages/toolkit/src/query/core/buildSlice.ts index ce089f0144..62a113a1e3 100644 --- a/packages/toolkit/src/query/core/buildSlice.ts +++ b/packages/toolkit/src/query/core/buildSlice.ts @@ -29,6 +29,9 @@ import type { } from './apiState' import { QueryStatus } from './apiState' import type { + AllQueryKeys, + QueryArgFromAnyQueryDefinition, + DataFromAnyQueryDefinition, InfiniteQueryThunk, MutationThunk, QueryThunk, @@ -64,11 +67,11 @@ import type { InternalSerializeQueryArgs } from '../defaultSerializeQueryArgs' */ export type NormalizedQueryUpsertEntry< Definitions extends EndpointDefinitions, - EndpointName extends QueryKeys, + EndpointName extends AllQueryKeys, > = { endpointName: EndpointName - arg: QueryArgFrom - value: ResultTypeFrom + arg: QueryArgFromAnyQueryDefinition + value: DataFromAnyQueryDefinition } /** @@ -89,7 +92,7 @@ export type ProcessedQueryUpsertEntry = { * A typesafe representation of a util action creator that accepts cache entry descriptions to upsert */ export type UpsertEntries = (< - EndpointNames extends Array>, + EndpointNames extends Array>, >( entries: [ ...{ diff --git a/packages/toolkit/src/query/core/buildThunks.ts b/packages/toolkit/src/query/core/buildThunks.ts index b5d4be5f72..b8df5856bc 100644 --- a/packages/toolkit/src/query/core/buildThunks.ts +++ b/packages/toolkit/src/query/core/buildThunks.ts @@ -6,6 +6,7 @@ import type { ThunkDispatch, UnknownAction, } from '@reduxjs/toolkit' +import util from 'util' import type { Patch } from 'immer' import { isDraftable, produceWithPatches } from 'immer' import type { Api, ApiContext } from '../apiTypes' @@ -19,8 +20,10 @@ import type { AssertTagTypes, EndpointDefinition, EndpointDefinitions, + InfiniteQueryArgFrom, InfiniteQueryDefinition, MutationDefinition, + PageParamFrom, QueryArgFrom, QueryDefinition, ResultTypeFrom, @@ -40,9 +43,11 @@ import type { InfiniteQueryConfigOptions, QueryCacheKey, InfiniteQueryDirection, + InfiniteQueryKeys, } from './apiState' import { QueryStatus } from './apiState' import type { + InfiniteQueryActionCreatorResult, QueryActionCreatorResult, StartInfiniteQueryActionCreatorOptions, StartQueryActionCreatorOptions, @@ -194,29 +199,80 @@ export type PatchQueryDataThunk< updateProvided?: boolean, ) => ThunkAction +export type AllQueryKeys = + | QueryKeys + | InfiniteQueryKeys + +export type QueryArgFromAnyQueryDefinition< + Definitions extends EndpointDefinitions, + EndpointName extends AllQueryKeys, +> = + Definitions[EndpointName] extends InfiniteQueryDefinition< + any, + any, + any, + any, + any + > + ? InfiniteQueryArgFrom + : Definitions[EndpointName] extends QueryDefinition + ? QueryArgFrom + : never + +export type DataFromAnyQueryDefinition< + Definitions extends EndpointDefinitions, + EndpointName extends AllQueryKeys, +> = + Definitions[EndpointName] extends InfiniteQueryDefinition< + any, + any, + any, + any, + any + > + ? InfiniteData< + ResultTypeFrom, + PageParamFrom + > + : Definitions[EndpointName] extends QueryDefinition + ? ResultTypeFrom + : unknown + +export type UpsertThunkResult< + Definitions extends EndpointDefinitions, + EndpointName extends AllQueryKeys, +> = + Definitions[EndpointName] extends InfiniteQueryDefinition< + any, + any, + any, + any, + any + > + ? InfiniteQueryActionCreatorResult + : Definitions[EndpointName] extends QueryDefinition + ? QueryActionCreatorResult + : QueryActionCreatorResult + export type UpdateQueryDataThunk< Definitions extends EndpointDefinitions, PartialState, -> = >( +> = >( endpointName: EndpointName, - arg: QueryArgFrom, - updateRecipe: Recipe>, + arg: QueryArgFromAnyQueryDefinition, + updateRecipe: Recipe>, updateProvided?: boolean, ) => ThunkAction export type UpsertQueryDataThunk< Definitions extends EndpointDefinitions, PartialState, -> = >( +> = >( endpointName: EndpointName, - arg: QueryArgFrom, - value: ResultTypeFrom, + arg: QueryArgFromAnyQueryDefinition, + value: DataFromAnyQueryDefinition, ) => ThunkAction< - QueryActionCreatorResult< - Definitions[EndpointName] extends QueryDefinition - ? Definitions[EndpointName] - : never - >, + UpsertThunkResult, PartialState, any, UnknownAction @@ -368,7 +424,8 @@ export function buildThunks< const upsertQueryData: UpsertQueryDataThunk = (endpointName, arg, value) => (dispatch) => { - return dispatch( + type EndpointName = typeof endpointName + const res = dispatch( ( api.endpoints[endpointName] as ApiEndpointQuery< QueryDefinition, @@ -381,7 +438,9 @@ export function buildThunks< data: value, }), }), - ) + ) as UpsertThunkResult + + return res } // The generic async payload function for all of our thunks @@ -582,6 +641,14 @@ export function buildThunks< // Fetch first page result = await fetchPage(existingData, firstPageParam, maxPages) + if (forceQueryFn) { + // HACK `upsertQueryData` expects the user to pass in the `{pages, pageParams}` structure, + // but `fetchPage` treats that as `pages[0]`. We have to manually un-nest it. + result = { + data: (result.data as InfiniteData).pages[0], + } as QueryReturnValue + } + // Fetch remaining pages for (let i = 1; i < totalPages; i++) { const param = getNextPageParam( diff --git a/packages/toolkit/src/query/tests/infiniteQueries.test.ts b/packages/toolkit/src/query/tests/infiniteQueries.test.ts index 8041cf26c5..91ef76b653 100644 --- a/packages/toolkit/src/query/tests/infiniteQueries.test.ts +++ b/packages/toolkit/src/query/tests/infiniteQueries.test.ts @@ -523,14 +523,14 @@ describe('Infinite queries', () => { ) const res1 = storeRef.store.dispatch( - pokemonApi.endpoints.getInfinitePokemon.initiate('fire', {}), + pokemonApiWithRefetch.endpoints.getInfinitePokemon.initiate('fire', {}), ) const entry1InitialLoad = await res1 checkResultData(entry1InitialLoad, [[{ id: '0', name: 'Pokemon 0' }]]) const res2 = storeRef.store.dispatch( - pokemonApi.endpoints.getInfinitePokemon.initiate('fire', { + pokemonApiWithRefetch.endpoints.getInfinitePokemon.initiate('fire', { direction: 'forward', }), ) @@ -541,4 +541,84 @@ describe('Infinite queries', () => { [{ id: '1', name: 'Pokemon 1' }], ]) }) + + test('Works with cache manipulation utils', async () => { + const res1 = storeRef.store.dispatch( + pokemonApi.endpoints.getInfinitePokemon.initiate('fire', {}), + ) + + const entry1InitialLoad = await res1 + checkResultData(entry1InitialLoad, [[{ id: '0', name: 'Pokemon 0' }]]) + + storeRef.store.dispatch( + pokemonApi.util.updateQueryData('getInfinitePokemon', 'fire', (draft) => { + draft.pages.push([{ id: '1', name: 'Pokemon 1' }]) + draft.pageParams.push(1) + }), + ) + + const selectFire = pokemonApi.endpoints.getInfinitePokemon.select('fire') + const entry1Updated = selectFire(storeRef.store.getState()) + + expect(entry1Updated.data).toEqual({ + pages: [ + [{ id: '0', name: 'Pokemon 0' }], + [{ id: '1', name: 'Pokemon 1' }], + ], + pageParams: [0, 1], + }) + + const res2 = storeRef.store.dispatch( + pokemonApi.util.upsertQueryData('getInfinitePokemon', 'water', { + pages: [[{ id: '2', name: 'Pokemon 2' }]], + pageParams: [2], + }), + ) + + const entry2InitialLoad = await res2 + const selectWater = pokemonApi.endpoints.getInfinitePokemon.select('water') + const entry2Updated = selectWater(storeRef.store.getState()) + + expect(entry2Updated.data).toEqual({ + pages: [[{ id: '2', name: 'Pokemon 2' }]], + pageParams: [2], + }) + + storeRef.store.dispatch( + pokemonApi.util.upsertQueryEntries([ + { + endpointName: 'getInfinitePokemon', + arg: 'air', + value: { + pages: [[{ id: '3', name: 'Pokemon 3' }]], + pageParams: [3], + }, + }, + ]), + ) + + const selectAir = pokemonApi.endpoints.getInfinitePokemon.select('air') + const entry3Initial = selectAir(storeRef.store.getState()) + + expect(entry3Initial.data).toEqual({ + pages: [[{ id: '3', name: 'Pokemon 3' }]], + pageParams: [3], + }) + + await storeRef.store.dispatch( + pokemonApi.endpoints.getInfinitePokemon.initiate('air', { + direction: 'forward', + }), + ) + + const entry3Updated = selectAir(storeRef.store.getState()) + + expect(entry3Updated.data).toEqual({ + pages: [ + [{ id: '3', name: 'Pokemon 3' }], + [{ id: '4', name: 'Pokemon 4' }], + ], + pageParams: [3, 4], + }) + }) })