From 881db121fe7c243d19d62be4b9fb8305ecf145c0 Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Wed, 14 Aug 2024 16:48:42 -0400 Subject: [PATCH 1/7] Extract reusable helpers for writing cache entries --- packages/toolkit/src/query/core/buildSlice.ts | 171 ++++++++++-------- 1 file changed, 96 insertions(+), 75 deletions(-) diff --git a/packages/toolkit/src/query/core/buildSlice.ts b/packages/toolkit/src/query/core/buildSlice.ts index f609e3a989..8ee42bd943 100644 --- a/packages/toolkit/src/query/core/buildSlice.ts +++ b/packages/toolkit/src/query/core/buildSlice.ts @@ -112,6 +112,100 @@ export function buildSlice({ > }) { const resetApiState = createAction(`${reducerPath}/resetApiState`) + + function writePendingCacheEntry( + draft: QueryState, + arg: QueryThunkArg, + upserting: boolean, + meta: { + arg: QueryThunkArg + requestId: string + // requestStatus: 'pending' + } & { startedTimeStamp: number }, + ) { + draft[arg.queryCacheKey] ??= { + status: QueryStatus.uninitialized, + endpointName: arg.endpointName, + } + + updateQuerySubstateIfExists(draft, arg.queryCacheKey, (substate) => { + substate.status = QueryStatus.pending + + substate.requestId = + upserting && substate.requestId + ? // for `upsertQuery` **updates**, keep the current `requestId` + substate.requestId + : // for normal queries or `upsertQuery` **inserts** always update the `requestId` + meta.requestId + if (arg.originalArgs !== undefined) { + substate.originalArgs = arg.originalArgs + } + substate.startedTimeStamp = meta.startedTimeStamp + }) + } + + function writeFulfilledCacheEntry( + draft: QueryState, + meta: { + arg: QueryThunkArg + requestId: string + // requestStatus: 'fulfilled' + } & { + fulfilledTimeStamp: number + baseQueryMeta: unknown + // RTK_autoBatch: true + }, + payload: unknown, + ) { + updateQuerySubstateIfExists(draft, meta.arg.queryCacheKey, (substate) => { + if (substate.requestId !== meta.requestId && !isUpsertQuery(meta.arg)) + return + const { merge } = definitions[meta.arg.endpointName] as QueryDefinition< + any, + any, + any, + any + > + substate.status = QueryStatus.fulfilled + + if (merge) { + if (substate.data !== undefined) { + const { fulfilledTimeStamp, arg, baseQueryMeta, requestId } = meta + // There's existing cache data. Let the user merge it in themselves. + // We're already inside an Immer-powered reducer, and the user could just mutate `substate.data` + // themselves inside of `merge()`. But, they might also want to return a new value. + // Try to let Immer figure that part out, save the result, and assign it to `substate.data`. + let newData = createNextState(substate.data, (draftSubstateData) => { + // As usual with Immer, you can mutate _or_ return inside here, but not both + return merge(draftSubstateData, payload, { + arg: arg.originalArgs, + baseQueryMeta, + fulfilledTimeStamp, + requestId, + }) + }) + substate.data = newData + } else { + // Presumably a fresh request. Just cache the response data. + substate.data = payload + } + } else { + // Assign or safely update the cache data. + substate.data = + definitions[meta.arg.endpointName].structuralSharing ?? true + ? copyWithStructuralSharing( + isDraft(substate.data) + ? original(substate.data) + : substate.data, + payload, + ) + : payload + } + + delete substate.error + substate.fulfilledTimeStamp = meta.fulfilledTimeStamp + }) + } const querySlice = createSlice({ name: `${reducerPath}/queries`, initialState: initialState as QueryState, @@ -149,83 +243,10 @@ export function buildSlice({ builder .addCase(queryThunk.pending, (draft, { meta, meta: { arg } }) => { const upserting = isUpsertQuery(arg) - draft[arg.queryCacheKey] ??= { - status: QueryStatus.uninitialized, - endpointName: arg.endpointName, - } - - updateQuerySubstateIfExists(draft, arg.queryCacheKey, (substate) => { - substate.status = QueryStatus.pending - - substate.requestId = - upserting && substate.requestId - ? // for `upsertQuery` **updates**, keep the current `requestId` - substate.requestId - : // for normal queries or `upsertQuery` **inserts** always update the `requestId` - meta.requestId - if (arg.originalArgs !== undefined) { - substate.originalArgs = arg.originalArgs - } - substate.startedTimeStamp = meta.startedTimeStamp - }) + writePendingCacheEntry(draft, arg, upserting, meta) }) .addCase(queryThunk.fulfilled, (draft, { meta, payload }) => { - updateQuerySubstateIfExists( - draft, - meta.arg.queryCacheKey, - (substate) => { - if ( - substate.requestId !== meta.requestId && - !isUpsertQuery(meta.arg) - ) - return - const { merge } = definitions[ - meta.arg.endpointName - ] as QueryDefinition - substate.status = QueryStatus.fulfilled - - if (merge) { - if (substate.data !== undefined) { - const { fulfilledTimeStamp, arg, baseQueryMeta, requestId } = - meta - // There's existing cache data. Let the user merge it in themselves. - // We're already inside an Immer-powered reducer, and the user could just mutate `substate.data` - // themselves inside of `merge()`. But, they might also want to return a new value. - // Try to let Immer figure that part out, save the result, and assign it to `substate.data`. - let newData = createNextState( - substate.data, - (draftSubstateData) => { - // As usual with Immer, you can mutate _or_ return inside here, but not both - return merge(draftSubstateData, payload, { - arg: arg.originalArgs, - baseQueryMeta, - fulfilledTimeStamp, - requestId, - }) - }, - ) - substate.data = newData - } else { - // Presumably a fresh request. Just cache the response data. - substate.data = payload - } - } else { - // Assign or safely update the cache data. - substate.data = - definitions[meta.arg.endpointName].structuralSharing ?? true - ? copyWithStructuralSharing( - isDraft(substate.data) - ? original(substate.data) - : substate.data, - payload, - ) - : payload - } - - delete substate.error - substate.fulfilledTimeStamp = meta.fulfilledTimeStamp - }, - ) + writeFulfilledCacheEntry(draft, meta, payload) }) .addCase( queryThunk.rejected, From 80792737d758c0b55b0a2069a9f1649f15dc520b Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Wed, 14 Aug 2024 16:49:24 -0400 Subject: [PATCH 2/7] First draft implementation of `upsertCacheEntries` --- packages/toolkit/src/query/core/buildSlice.ts | 110 +++++++++++++++++- packages/toolkit/src/query/core/module.ts | 7 +- .../query/tests/optimisticUpserts.test.tsx | 48 ++++++++ 3 files changed, 163 insertions(+), 2 deletions(-) diff --git a/packages/toolkit/src/query/core/buildSlice.ts b/packages/toolkit/src/query/core/buildSlice.ts index 8ee42bd943..77daeb7729 100644 --- a/packages/toolkit/src/query/core/buildSlice.ts +++ b/packages/toolkit/src/query/core/buildSlice.ts @@ -8,6 +8,8 @@ import { isRejectedWithValue, createNextState, prepareAutoBatched, + SHOULD_AUTOBATCH, + nanoid, } from './rtkImports' import type { QuerySubstateIdentifier, @@ -21,15 +23,24 @@ import type { QueryCacheKey, SubscriptionState, ConfigState, + QueryKeys, } from './apiState' import { QueryStatus } from './apiState' -import type { MutationThunk, QueryThunk, RejectedAction } from './buildThunks' +import type { + MutationThunk, + QueryThunk, + QueryThunkArg, + RejectedAction, +} from './buildThunks' import { calculateProvidedByThunk } from './buildThunks' import type { AssertTagTypes, + DefinitionType, EndpointDefinitions, FullTagDescription, + QueryArgFrom, QueryDefinition, + ResultTypeFrom, } from '../endpointDefinitions' import type { Patch } from 'immer' import { isDraft } from 'immer' @@ -42,6 +53,44 @@ import { } from '../utils' import type { ApiContext } from '../apiTypes' import { isUpsertQuery } from './buildInitiate' +import type { InternalSerializeQueryArgs } from '../defaultSerializeQueryArgs' + +/** + * A typesafe single entry to be upserted into the cache + */ +export type NormalizedQueryUpsertEntry< + Definitions extends EndpointDefinitions, + EndpointName extends QueryKeys, +> = { + endpointName: EndpointName + args: QueryArgFrom + value: ResultTypeFrom +} + +/** + * The internal version that is not typesafe since we can't carry the generics through `createSlice` + */ +type NormalizedQueryUpsertEntryPayload = { + endpointName: string + args: any + value: any +} + +/** + * A typesafe representation of a util action creator that accepts cache entry descriptions to upsert + */ +export type UpsertEntries = < + EndpointNames extends Array>, +>( + entries: [ + ...{ + [I in keyof EndpointNames]: NormalizedQueryUpsertEntry< + Definitions, + EndpointNames[I] + > + }, + ], +) => PayloadAction function updateQuerySubstateIfExists( state: QueryState, @@ -92,6 +141,7 @@ export function buildSlice({ reducerPath, queryThunk, mutationThunk, + serializeQueryArgs, context: { endpointDefinitions: definitions, apiUid, @@ -104,6 +154,7 @@ export function buildSlice({ reducerPath: string queryThunk: QueryThunk mutationThunk: MutationThunk + serializeQueryArgs: InternalSerializeQueryArgs context: ApiContext assertTagType: AssertTagTypes config: Omit< @@ -221,6 +272,63 @@ export function buildSlice({ }, prepare: prepareAutoBatched(), }, + cacheEntriesUpserted: { + reducer( + draft, + action: PayloadAction< + NormalizedQueryUpsertEntryPayload[], + string, + { + RTK_autoBatch: boolean + requestId: string + timestamp: number + } + >, + ) { + for (const entry of action.payload) { + const { endpointName, args, value } = entry + const endpointDefinition = definitions[endpointName] + + const arg: QueryThunkArg = { + type: 'query', + endpointName: endpointName, + originalArgs: entry.args, + queryCacheKey: serializeQueryArgs({ + queryArgs: args, + endpointDefinition, + endpointName, + }), + } + writePendingCacheEntry(draft, arg, true, { + arg, + requestId: action.meta.requestId, + startedTimeStamp: action.meta.timestamp, + }) + + writeFulfilledCacheEntry( + draft, + { + arg, + requestId: action.meta.requestId, + fulfilledTimeStamp: action.meta.timestamp, + baseQueryMeta: {}, + }, + entry.value, + ) + } + }, + prepare: (payload: NormalizedQueryUpsertEntryPayload[]) => { + const result = { + payload, + meta: { + [SHOULD_AUTOBATCH]: true, + requestId: nanoid(), + timestamp: Date.now(), + }, + } + return result + }, + }, queryResultPatched: { reducer( draft, diff --git a/packages/toolkit/src/query/core/module.ts b/packages/toolkit/src/query/core/module.ts index 6ed4d27cc3..afc3feef3e 100644 --- a/packages/toolkit/src/query/core/module.ts +++ b/packages/toolkit/src/query/core/module.ts @@ -47,7 +47,7 @@ import type { BuildSelectorsApiEndpointQuery, } from './buildSelectors' import { buildSelectors } from './buildSelectors' -import type { SliceActions } from './buildSlice' +import type { SliceActions, UpsertEntries } from './buildSlice' import { buildSlice } from './buildSlice' import type { BuildThunksApiEndpointMutation, @@ -320,6 +320,9 @@ export interface ApiModules< * ``` */ resetApiState: SliceActions['resetApiState'] + + upsertEntries: UpsertEntries + /** * A Redux action creator that can be used to manually invalidate cache tags for [automated re-fetching](../../usage/automated-refetching.mdx). * @@ -527,6 +530,7 @@ export const coreModule = ({ context, queryThunk, mutationThunk, + serializeQueryArgs, reducerPath, assertTagType, config: { @@ -545,6 +549,7 @@ export const coreModule = ({ upsertQueryData, prefetch, resetApiState: sliceActions.resetApiState, + upsertEntries: sliceActions.cacheEntriesUpserted as any, }) safeAssign(api.internalActions, sliceActions) diff --git a/packages/toolkit/src/query/tests/optimisticUpserts.test.tsx b/packages/toolkit/src/query/tests/optimisticUpserts.test.tsx index e2dba60ee2..673266ab16 100644 --- a/packages/toolkit/src/query/tests/optimisticUpserts.test.tsx +++ b/packages/toolkit/src/query/tests/optimisticUpserts.test.tsx @@ -27,6 +27,9 @@ const api = createApi({ }, tagTypes: ['Post'], endpoints: (build) => ({ + getPosts: build.query({ + query: () => '/posts', + }), post: build.query({ query: (id) => `post/${id}`, providesTags: ['Post'], @@ -327,6 +330,51 @@ describe('upsertQueryData', () => { }) }) +describe('upsertEntries', () => { + test('Upserts many entries at once', async () => { + const posts: Post[] = [ + { + id: '1', + contents: 'A', + title: 'A', + }, + { + id: '2', + contents: 'B', + title: 'B', + }, + { + id: '3', + contents: 'C', + title: 'C', + }, + ] + + storeRef.store.dispatch( + api.util.upsertEntries([ + { + endpointName: 'getPosts', + args: undefined, + value: posts, + }, + ...posts.map((post) => ({ + endpointName: 'post' as const, + args: post.id, + value: post, + })), + ]), + ) + + const state = storeRef.store.getState() + + expect(api.endpoints.getPosts.select()(state).data).toBe(posts) + + expect(api.endpoints.post.select('1')(state).data).toBe(posts[0]) + expect(api.endpoints.post.select('2')(state).data).toBe(posts[1]) + expect(api.endpoints.post.select('3')(state).data).toBe(posts[2]) + }) +}) + describe('full integration', () => { test('success case', async () => { baseQuery From ce7050f56bb6ab3ec58682cfa0405f0648b023a7 Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Mon, 2 Sep 2024 18:39:17 -0400 Subject: [PATCH 3/7] Handle batched upserts in cache lifecycles --- .../core/buildMiddleware/cacheCollection.ts | 36 +++-- .../core/buildMiddleware/cacheLifecycle.ts | 76 +++++++---- packages/toolkit/src/query/core/buildSlice.ts | 45 ++++--- .../query/tests/optimisticUpserts.test.tsx | 124 +++++++++++++----- 4 files changed, 196 insertions(+), 85 deletions(-) diff --git a/packages/toolkit/src/query/core/buildMiddleware/cacheCollection.ts b/packages/toolkit/src/query/core/buildMiddleware/cacheCollection.ts index 881fd9e11d..8b3ae6b198 100644 --- a/packages/toolkit/src/query/core/buildMiddleware/cacheCollection.ts +++ b/packages/toolkit/src/query/core/buildMiddleware/cacheCollection.ts @@ -44,12 +44,14 @@ export const buildCacheCollectionHandler: InternalHandlerBuilder = ({ context, internalState, }) => { - const { removeQueryResult, unsubscribeQueryResult } = api.internalActions + const { removeQueryResult, unsubscribeQueryResult, cacheEntriesUpserted } = + api.internalActions const canTriggerUnsubscribe = isAnyOf( unsubscribeQueryResult.match, queryThunk.fulfilled, queryThunk.rejected, + cacheEntriesUpserted.match, ) function anySubscriptionsRemainingForKey(queryCacheKey: string) { @@ -66,16 +68,27 @@ export const buildCacheCollectionHandler: InternalHandlerBuilder = ({ ) => { if (canTriggerUnsubscribe(action)) { const state = mwApi.getState()[reducerPath] - const { queryCacheKey } = unsubscribeQueryResult.match(action) - ? action.payload - : action.meta.arg - - handleUnsubscribe( - queryCacheKey, - state.queries[queryCacheKey]?.endpointName, - mwApi, - state.config, - ) + let queryCacheKeys: QueryCacheKey[] + + if (cacheEntriesUpserted.match(action)) { + queryCacheKeys = action.payload.map( + (entry) => entry.queryDescription.queryCacheKey, + ) + } else { + const { queryCacheKey } = unsubscribeQueryResult.match(action) + ? action.payload + : action.meta.arg + queryCacheKeys = [queryCacheKey] + } + + for (const queryCacheKey of queryCacheKeys) { + handleUnsubscribe( + queryCacheKey, + state.queries[queryCacheKey]?.endpointName, + mwApi, + state.config, + ) + } } if (api.util.resetApiState.match(action)) { @@ -132,6 +145,7 @@ export const buildCacheCollectionHandler: InternalHandlerBuilder = ({ if (currentTimeout) { clearTimeout(currentTimeout) } + currentRemovalTimeouts[queryCacheKey] = setTimeout(() => { if (!anySubscriptionsRemainingForKey(queryCacheKey)) { api.dispatch(removeQueryResult({ queryCacheKey })) diff --git a/packages/toolkit/src/query/core/buildMiddleware/cacheLifecycle.ts b/packages/toolkit/src/query/core/buildMiddleware/cacheLifecycle.ts index ebbbc727bb..72f84a0c54 100644 --- a/packages/toolkit/src/query/core/buildMiddleware/cacheLifecycle.ts +++ b/packages/toolkit/src/query/core/buildMiddleware/cacheLifecycle.ts @@ -183,6 +183,30 @@ export const buildCacheLifecycleHandler: InternalHandlerBuilder = ({ } const lifecycleMap: Record = {} + function resolveLifecycleEntry( + cacheKey: string, + data: unknown, + meta: unknown, + ) { + const lifecycle = lifecycleMap[cacheKey] + + if (lifecycle?.valueResolved) { + lifecycle.valueResolved({ + data, + meta, + }) + delete lifecycle.valueResolved + } + } + + function removeLifecycleEntry(cacheKey: string) { + const lifecycle = lifecycleMap[cacheKey] + if (lifecycle) { + delete lifecycleMap[cacheKey] + lifecycle.cacheEntryRemoved() + } + } + const handler: ApiMiddlewareInternalHandler = ( action, mwApi, @@ -190,17 +214,37 @@ export const buildCacheLifecycleHandler: InternalHandlerBuilder = ({ ) => { const cacheKey = getCacheKey(action) - if (queryThunk.pending.match(action)) { + function checkForNewCacheKey( + endpointName: string, + cacheKey: string, + requestId: string, + originalArgs: unknown, + ) { const oldState = stateBefore[reducerPath].queries[cacheKey] const state = mwApi.getState()[reducerPath].queries[cacheKey] if (!oldState && state) { - handleNewKey( - action.meta.arg.endpointName, - action.meta.arg.originalArgs, - cacheKey, - mwApi, + handleNewKey(endpointName, originalArgs, cacheKey, mwApi, requestId) + } + } + + if (queryThunk.pending.match(action)) { + checkForNewCacheKey( + action.meta.arg.endpointName, + cacheKey, + action.meta.requestId, + action.meta.arg.originalArgs, + ) + } else if (api.internalActions.cacheEntriesUpserted.match(action)) { + for (const { queryDescription, value } of action.payload) { + const { endpointName, originalArgs, queryCacheKey } = queryDescription + checkForNewCacheKey( + endpointName, + queryCacheKey, action.meta.requestId, + originalArgs, ) + + resolveLifecycleEntry(queryCacheKey, value, {}) } } else if (mutationThunk.pending.match(action)) { const state = mwApi.getState()[reducerPath].mutations[cacheKey] @@ -214,27 +258,15 @@ export const buildCacheLifecycleHandler: InternalHandlerBuilder = ({ ) } } else if (isFulfilledThunk(action)) { - const lifecycle = lifecycleMap[cacheKey] - if (lifecycle?.valueResolved) { - lifecycle.valueResolved({ - data: action.payload, - meta: action.meta.baseQueryMeta, - }) - delete lifecycle.valueResolved - } + resolveLifecycleEntry(cacheKey, action.payload, action.meta.baseQueryMeta) } else if ( api.internalActions.removeQueryResult.match(action) || api.internalActions.removeMutationResult.match(action) ) { - const lifecycle = lifecycleMap[cacheKey] - if (lifecycle) { - delete lifecycleMap[cacheKey] - lifecycle.cacheEntryRemoved() - } + removeLifecycleEntry(cacheKey) } else if (api.util.resetApiState.match(action)) { - for (const [cacheKey, lifecycle] of Object.entries(lifecycleMap)) { - delete lifecycleMap[cacheKey] - lifecycle.cacheEntryRemoved() + for (const cacheKey of Object.keys(lifecycleMap)) { + removeLifecycleEntry(cacheKey) } } } diff --git a/packages/toolkit/src/query/core/buildSlice.ts b/packages/toolkit/src/query/core/buildSlice.ts index 77daeb7729..c9652bf766 100644 --- a/packages/toolkit/src/query/core/buildSlice.ts +++ b/packages/toolkit/src/query/core/buildSlice.ts @@ -76,6 +76,11 @@ type NormalizedQueryUpsertEntryPayload = { value: any } +export type ProcessedQueryUpsertEntry = { + queryDescription: QueryThunkArg + value: unknown +} + /** * A typesafe representation of a util action creator that accepts cache entry descriptions to upsert */ @@ -90,7 +95,7 @@ export type UpsertEntries = < > }, ], -) => PayloadAction +) => PayloadAction function updateQuerySubstateIfExists( state: QueryState, @@ -276,7 +281,7 @@ export function buildSlice({ reducer( draft, action: PayloadAction< - NormalizedQueryUpsertEntryPayload[], + ProcessedQueryUpsertEntry[], string, { RTK_autoBatch: boolean @@ -286,19 +291,7 @@ export function buildSlice({ >, ) { for (const entry of action.payload) { - const { endpointName, args, value } = entry - const endpointDefinition = definitions[endpointName] - - const arg: QueryThunkArg = { - type: 'query', - endpointName: endpointName, - originalArgs: entry.args, - queryCacheKey: serializeQueryArgs({ - queryArgs: args, - endpointDefinition, - endpointName, - }), - } + const { queryDescription: arg, value } = entry writePendingCacheEntry(draft, arg, true, { arg, requestId: action.meta.requestId, @@ -313,13 +306,31 @@ export function buildSlice({ fulfilledTimeStamp: action.meta.timestamp, baseQueryMeta: {}, }, - entry.value, + value, ) } }, prepare: (payload: NormalizedQueryUpsertEntryPayload[]) => { + const queryDescriptions: ProcessedQueryUpsertEntry[] = payload.map( + (entry) => { + const { endpointName, args, value } = entry + const endpointDefinition = definitions[endpointName] + const queryDescription: QueryThunkArg = { + type: 'query', + endpointName: endpointName, + originalArgs: entry.args, + queryCacheKey: serializeQueryArgs({ + queryArgs: args, + endpointDefinition, + endpointName, + }), + } + return { queryDescription, value } + }, + ) + const result = { - payload, + payload: queryDescriptions, meta: { [SHOULD_AUTOBATCH]: true, requestId: nanoid(), diff --git a/packages/toolkit/src/query/tests/optimisticUpserts.test.tsx b/packages/toolkit/src/query/tests/optimisticUpserts.test.tsx index 673266ab16..5c0b244390 100644 --- a/packages/toolkit/src/query/tests/optimisticUpserts.test.tsx +++ b/packages/toolkit/src/query/tests/optimisticUpserts.test.tsx @@ -1,4 +1,5 @@ import { createApi } from '@reduxjs/toolkit/query/react' +import { createAction } from '@reduxjs/toolkit' import { actionsReducer, hookWaitFor, @@ -16,6 +17,8 @@ interface Post { const baseQuery = vi.fn() beforeEach(() => baseQuery.mockReset()) +const postAddedAction = createAction('postAdded') + const api = createApi({ baseQuery: (...args: any[]) => { const result = baseQuery(...args) @@ -65,6 +68,18 @@ const api = createApi({ } }, }), + postWithSideEffect: build.query({ + query: (id) => `post/${id}`, + providesTags: ['Post'], + async onCacheEntryAdded(arg, api) { + // Verify that lifecycle promise resolution works + const res = await api.cacheDataLoaded + + // and leave a side effect we can check in the test + api.dispatch(postAddedAction(res.data.id)) + }, + keepUnusedDataFor: 0.01, + }), }), }) @@ -331,47 +346,86 @@ describe('upsertQueryData', () => { }) describe('upsertEntries', () => { + const posts: Post[] = [ + { + id: '1', + contents: 'A', + title: 'A', + }, + { + id: '2', + contents: 'B', + title: 'B', + }, + { + id: '3', + contents: 'C', + title: 'C', + }, + ] + + const entriesAction = api.util.upsertEntries([ + { + endpointName: 'getPosts', + args: undefined, + value: posts, + }, + ...posts.map((post) => ({ + endpointName: 'postWithSideEffect' as const, + args: post.id, + value: post, + })), + ]) + test('Upserts many entries at once', async () => { - const posts: Post[] = [ - { - id: '1', - contents: 'A', - title: 'A', - }, - { - id: '2', - contents: 'B', - title: 'B', - }, - { - id: '3', - contents: 'C', - title: 'C', - }, - ] - - storeRef.store.dispatch( - api.util.upsertEntries([ - { - endpointName: 'getPosts', - args: undefined, - value: posts, - }, - ...posts.map((post) => ({ - endpointName: 'post' as const, - args: post.id, - value: post, - })), - ]), - ) + storeRef.store.dispatch(entriesAction) const state = storeRef.store.getState() expect(api.endpoints.getPosts.select()(state).data).toBe(posts) - expect(api.endpoints.post.select('1')(state).data).toBe(posts[0]) - expect(api.endpoints.post.select('2')(state).data).toBe(posts[1]) - expect(api.endpoints.post.select('3')(state).data).toBe(posts[2]) + expect(api.endpoints.postWithSideEffect.select('1')(state).data).toBe( + posts[0], + ) + expect(api.endpoints.postWithSideEffect.select('2')(state).data).toBe( + posts[1], + ) + expect(api.endpoints.postWithSideEffect.select('3')(state).data).toBe( + posts[2], + ) + }) + + test('Triggers cache lifecycles and side effects', async () => { + storeRef.store.dispatch(entriesAction) + + // Tricky timing. The cache data promises will be resolved + // in microtasks, so we just need any async delay here. + await delay(1) + + const state = storeRef.store.getState() + + // onCacheEntryAdded should have run for each post, + // including cache data being resolved + for (const post of posts) { + const matchingSideEffectAction = state.actions.find( + (action) => postAddedAction.match(action) && action.payload === post.id, + ) + expect(matchingSideEffectAction).toBeTruthy() + } + + expect(api.endpoints.postWithSideEffect.select('1')(state).data).toBe( + posts[0], + ) + + // The cache data should be removed after the keepUnusedDataFor time, + // so wait longer than that + await delay(20) + + const stateAfter = storeRef.store.getState() + + expect(api.endpoints.postWithSideEffect.select('1')(stateAfter).data).toBe( + undefined, + ) }) }) From ef73a9066beb9f3c8702a1b62141247eef65c809 Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Mon, 14 Oct 2024 15:20:46 -0400 Subject: [PATCH 4/7] Rename to `upsertQueryEntries` and entry field `arg` --- packages/toolkit/src/query/core/buildSlice.ts | 12 ++++++------ packages/toolkit/src/query/core/module.ts | 4 ++-- .../src/query/tests/optimisticUpserts.test.tsx | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/toolkit/src/query/core/buildSlice.ts b/packages/toolkit/src/query/core/buildSlice.ts index c9652bf766..11e0c863c5 100644 --- a/packages/toolkit/src/query/core/buildSlice.ts +++ b/packages/toolkit/src/query/core/buildSlice.ts @@ -63,7 +63,7 @@ export type NormalizedQueryUpsertEntry< EndpointName extends QueryKeys, > = { endpointName: EndpointName - args: QueryArgFrom + arg: QueryArgFrom value: ResultTypeFrom } @@ -72,8 +72,8 @@ export type NormalizedQueryUpsertEntry< */ type NormalizedQueryUpsertEntryPayload = { endpointName: string - args: any - value: any + arg: unknown + value: unknown } export type ProcessedQueryUpsertEntry = { @@ -313,14 +313,14 @@ export function buildSlice({ prepare: (payload: NormalizedQueryUpsertEntryPayload[]) => { const queryDescriptions: ProcessedQueryUpsertEntry[] = payload.map( (entry) => { - const { endpointName, args, value } = entry + const { endpointName, arg, value } = entry const endpointDefinition = definitions[endpointName] const queryDescription: QueryThunkArg = { type: 'query', endpointName: endpointName, - originalArgs: entry.args, + originalArgs: entry.arg, queryCacheKey: serializeQueryArgs({ - queryArgs: args, + queryArgs: arg, endpointDefinition, endpointName, }), diff --git a/packages/toolkit/src/query/core/module.ts b/packages/toolkit/src/query/core/module.ts index afc3feef3e..06822cb039 100644 --- a/packages/toolkit/src/query/core/module.ts +++ b/packages/toolkit/src/query/core/module.ts @@ -321,7 +321,7 @@ export interface ApiModules< */ resetApiState: SliceActions['resetApiState'] - upsertEntries: UpsertEntries + upsertQueryEntries: UpsertEntries /** * A Redux action creator that can be used to manually invalidate cache tags for [automated re-fetching](../../usage/automated-refetching.mdx). @@ -549,7 +549,7 @@ export const coreModule = ({ upsertQueryData, prefetch, resetApiState: sliceActions.resetApiState, - upsertEntries: sliceActions.cacheEntriesUpserted as any, + upsertQueryEntries: sliceActions.cacheEntriesUpserted as any, }) safeAssign(api.internalActions, sliceActions) diff --git a/packages/toolkit/src/query/tests/optimisticUpserts.test.tsx b/packages/toolkit/src/query/tests/optimisticUpserts.test.tsx index 5c0b244390..600c433aab 100644 --- a/packages/toolkit/src/query/tests/optimisticUpserts.test.tsx +++ b/packages/toolkit/src/query/tests/optimisticUpserts.test.tsx @@ -345,7 +345,7 @@ describe('upsertQueryData', () => { }) }) -describe('upsertEntries', () => { +describe('upsertQueryEntries', () => { const posts: Post[] = [ { id: '1', @@ -364,15 +364,15 @@ describe('upsertEntries', () => { }, ] - const entriesAction = api.util.upsertEntries([ + const entriesAction = api.util.upsertQueryEntries([ { endpointName: 'getPosts', - args: undefined, + arg: undefined, value: posts, }, ...posts.map((post) => ({ endpointName: 'postWithSideEffect' as const, - args: post.id, + arg: post.id, value: post, })), ]) From 5211b1801a94811fa3e09268ad3e5a31dc3912c8 Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Mon, 14 Oct 2024 15:21:28 -0400 Subject: [PATCH 5/7] Add API docs for `upsertQueryEntries` --- .../api/created-api/api-slice-utils.mdx | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/docs/rtk-query/api/created-api/api-slice-utils.mdx b/docs/rtk-query/api/created-api/api-slice-utils.mdx index 5ce2f72e07..91d8e984e3 100644 --- a/docs/rtk-query/api/created-api/api-slice-utils.mdx +++ b/docs/rtk-query/api/created-api/api-slice-utils.mdx @@ -197,6 +197,86 @@ dispatch( patchCollection.undo() ``` +### `upsertQueryEntries` + +A standard Redux action creator that accepts an array of individual cache entry descriptions, and immediately upserts them into the store. This is designed to efficiently bulk-insert many entries at once. + +#### Signature + +```ts no-transpile +/** + * A typesafe single entry to be upserted into the cache + */ +export type NormalizedQueryUpsertEntry< + Definitions extends EndpointDefinitions, + EndpointName extends QueryKeys, +> = { + endpointName: EndpointName + arg: QueryArgFrom + value: ResultTypeFrom +} + +const upsertQueryEntries = (entries: NormalizedQueryUpsertEntry[]) => + PayloadAction +``` + +- **Parameters** + - `entries`: an array of objects that contain the data needed to upsert individual cache entries: + - `endpointName`: the name of the endpoint, such as `"getPokemon"` + - `arg`: the full query key argument needed to identify this cache entry, such as `"pikachu"` (same as you would pass to a `useQuery` hook or `api.endpoints.someEndpoint.select()`) + - `value`: the data to be upserted into this cache entry, exactly as formatted. + +#### Description + +This method is designed as a more efficient approach to bulk-inserting many entries at once than many individual calls to `upsertQueryData`. As a comparison: + +- `upsertQueryData`: + - upserts one cache entry at a time + - Is async + - Dispatches 2 separate actions, `pending` and `fulfilled` + - Runs the `transformResponse` callback if defined for that endpoint, as well as the `merge` callback if defined +- `upsertQueryEntries`: + - upserts many cache entries at once, and they may be for any combination of endpoints defined in the API + - Is a single synchronous action + - Does _not_ run `transformResponse`, so the provided `value` fields must already be in the final format expected for that endpoint. However, it will still run the `merge` callback if defined + +Currently, this method has two main use cases. The first is prefilling the cache with data retrieved from storage on app startup. The second is to act as a "pseudo-normalization" tool. [RTK Query is _not_ a "normalized" cache](../../usage/cache-behavior.mdx#no-normalized-or-de-duplicated-cache). However, there are times when you may want to prefill other cache entries with the contents of another endpoint, such as taking the results of a `getPosts` list endpoint response and prefilling the individual `getPost(id)` endpoint cache entries. + +If no cache entry for that cache key exists, a cache entry will be created and the data added. If a cache entry already exists, this will _overwrite_ the existing cache entry data. + +If dispatched while an actual request is in progress, both the upsert and request will be handled as soon as they resolve, resulting in a "last result wins" update behavior. + +#### Example + +```ts no-transpile +const api = createApi({ + endpoints: (build) => ({ + getPosts: build.query({ + query: () => '/posts', + async onQueryStarted(_, { dispatch, queryFulfilled }) { + const res = await queryFulfilled + const posts = res.data + + // Pre-fill the individual post entries with the results + // from the list endpoint query + const entries = dispatch( + api.util.upsertQueryEntries( + posts.map((post) => ({ + endpointName: 'getPost', + arg: { id: post.id }, + value: post, + })), + ), + ) + }, + }), + getPost: build.query>({ + query: (post) => `post/${post.id}`, + }), + }), +}) +``` + ### `prefetch` #### Signature From b71222ddb00a6074dbdf1d8c4071c74e4cb0b100 Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Mon, 14 Oct 2024 15:22:13 -0400 Subject: [PATCH 6/7] Consistently use `arg` name --- .../api/created-api/api-slice-utils.mdx | 50 +++++++++---------- .../toolkit/src/query/core/buildThunks.ts | 31 +++++------- packages/toolkit/src/query/core/module.ts | 8 +-- 3 files changed, 42 insertions(+), 47 deletions(-) diff --git a/docs/rtk-query/api/created-api/api-slice-utils.mdx b/docs/rtk-query/api/created-api/api-slice-utils.mdx index 91d8e984e3..18d9dcbf6f 100644 --- a/docs/rtk-query/api/created-api/api-slice-utils.mdx +++ b/docs/rtk-query/api/created-api/api-slice-utils.mdx @@ -23,12 +23,14 @@ Some of the TS types on this page are pseudocode to illustrate intent, as the ac ### `updateQueryData` +A Redux thunk action creator that, when dispatched, creates and applies a set of JSON diff/patch objects to the current state. This immediately updates the Redux state with those changes. + #### Signature ```ts no-transpile const updateQueryData = ( endpointName: string, - args: any, + arg: any, updateRecipe: (draft: Draft) => void, updateProvided?: boolean, ) => ThunkAction @@ -42,21 +44,19 @@ interface PatchCollection { - **Parameters** - `endpointName`: a string matching an existing endpoint name - - `args`: an argument matching that used for a previous query call, used to determine which cached dataset needs to be updated + - `arg`: an argument matching that used for a previous query call, used to determine which cached dataset needs to be updated - `updateRecipe`: an Immer `produce` callback that can apply changes to the cached state - `updateProvided`: a boolean indicating whether the endpoint's provided tags should be re-calculated based on the updated cache. Defaults to `false`. #### Description -A Redux thunk action creator that, when dispatched, creates and applies a set of JSON diff/patch objects to the current state. This immediately updates the Redux state with those changes. - The thunk action creator accepts three arguments: the name of the endpoint we are updating (such as `'getPost'`), any relevant query arguments, and a callback function. The callback receives an Immer-wrapped `draft` of the current state, and may modify the draft to match the expected results after the mutation completes successfully. The thunk returns an object containing `{patches: Patch[], inversePatches: Patch[], undo: () => void}`. The `patches` and `inversePatches` are generated using Immer's [`produceWithPatches` method](https://immerjs.github.io/immer/patches). -This is typically used as the first step in implementing optimistic updates. The generated `inversePatches` can be used to revert the updates by calling `dispatch(patchQueryData(endpointName, args, inversePatches))`. Alternatively, the `undo` method can be called directly to achieve the same effect. +This is typically used as the first step in implementing optimistic updates. The generated `inversePatches` can be used to revert the updates by calling `dispatch(patchQueryData(endpointName, arg, inversePatches))`. Alternatively, the `undo` method can be called directly to achieve the same effect. -Note that the first two arguments (`endpointName` and `args`) are used to determine which existing cache entry to update. If no existing cache entry is found, the `updateRecipe` callback will not run. +Note that the first two arguments (`endpointName` and `arg`) are used to determine which existing cache entry to update. If no existing cache entry is found, the `updateRecipe` callback will not run. #### Example 1 @@ -69,7 +69,7 @@ const patchCollection = dispatch( ``` In the example above, `'getPosts'` is provided for the `endpointName`, and `undefined` is provided -for `args`. This will match a query cache key of `'getPosts(undefined)'`. +for `arg`. This will match a query cache key of `'getPosts(undefined)'`. i.e. it will match a cache entry that may have been created via any of the following calls: @@ -96,7 +96,7 @@ const patchCollection = dispatch( ``` In the example above, `'getPostById'` is provided for the `endpointName`, and `1` is provided -for `args`. This will match a query cache key of `'getPostById(1)'`. +for `arg`. This will match a query cache key of `'getPostById(1)'`. i.e. it will match a cache entry that may have been created via any of the following calls: @@ -114,27 +114,27 @@ dispatch(api.endpoints.getPostById.initiate(1, { ...options })) ### `upsertQueryData` +A Redux thunk action creator that, when dispatched, acts as an artificial API request to upsert a value into the cache. + #### Signature ```ts no-transpile -const upsertQueryData = (endpointName: string, args: any, newEntryData: T) => +const upsertQueryData = (endpointName: string, arg: any, newEntryData: T) => ThunkAction>, PartialState, any, UnknownAction> ``` - **Parameters** - `endpointName`: a string matching an existing endpoint name - - `args`: an argument matching that used for a previous query call, used to determine which cached dataset needs to be updated + - `arg`: an argument matching that used for a previous query call, used to determine which cached dataset needs to be updated - `newEntryValue`: the value to be written into the corresponding cache entry's `data` field #### Description -A Redux thunk action creator that, when dispatched, acts as an artificial API request to upsert a value into the cache. - The thunk action creator accepts three arguments: the name of the endpoint we are updating (such as `'getPost'`), the appropriate query arg values to construct the desired cache key, and the data to upsert. If no cache entry for that cache key exists, a cache entry will be created and the data added. If a cache entry already exists, this will _overwrite_ the existing cache entry data. -The thunk executes _asynchronously_, and returns a promise that resolves when the store has been updated. +The thunk executes _asynchronously_, and returns a promise that resolves when the store has been updated. This includes executing the `transformResponse` callback if defined for that endpoint. If dispatched while an actual request is in progress, both the upsert and request will be handled as soon as they resolve, resulting in a "last result wins" update behavior. @@ -148,12 +148,14 @@ await dispatch( ### `patchQueryData` +A Redux thunk action creator that, when dispatched, applies a JSON diff/patch array to the cached data for a given query result. This immediately updates the Redux state with those changes. + #### Signature ```ts no-transpile const patchQueryData = ( endpointName: string, - args: any + arg: any patches: Patch[], updateProvided?: boolean ) => ThunkAction; @@ -161,14 +163,12 @@ const patchQueryData = ( - **Parameters** - `endpointName`: a string matching an existing endpoint name - - `args`: a cache key, used to determine which cached dataset needs to be updated + - `arg`: a cache key, used to determine which cached dataset needs to be updated - `patches`: an array of patches (or inverse patches) to apply to cached state. These would typically be obtained from the result of dispatching [`updateQueryData`](#updatequerydata) - `updateProvided`: a boolean indicating whether the endpoint's provided tags should be re-calculated based on the updated cache. Defaults to `false`. #### Description -A Redux thunk action creator that, when dispatched, applies a JSON diff/patch array to the cached data for a given query result. This immediately updates the Redux state with those changes. - The thunk action creator accepts three arguments: the name of the endpoint we are updating (such as `'getPost'`), the appropriate query arg values to construct the desired cache key, and a JSON diff/patch array as produced by Immer's `produceWithPatches`. This is typically used as the second step in implementing optimistic updates. If a request fails, the optimistically-applied changes can be reverted by dispatching `patchQueryData` with the `inversePatches` that were generated by `updateQueryData` earlier. @@ -279,6 +279,8 @@ const api = createApi({ ### `prefetch` +A Redux thunk action creator that can be used to manually trigger pre-fetching of data. + #### Signature ```ts no-transpile @@ -298,8 +300,6 @@ const prefetch = (endpointName: string, arg: any, options: PrefetchOptions) => #### Description -A Redux thunk action creator that can be used to manually trigger pre-fetching of data. - The thunk action creator accepts three arguments: the name of the endpoint we are updating (such as `'getPost'`), any relevant query arguments, and a set of options used to determine if the data actually should be re-fetched based on cache staleness. React Hooks users will most likely never need to use this directly, as the `usePrefetch` hook will dispatch the thunk action creator result internally as needed when you call the prefetching function supplied by the hook. @@ -312,6 +312,8 @@ dispatch(api.util.prefetch('getPosts', undefined, { force: true })) ### `selectInvalidatedBy` +A selector function that can select query parameters to be invalidated. + #### Signature ```ts no-transpile @@ -334,8 +336,6 @@ function selectInvalidatedBy( #### Description -A function that can select query parameters to be invalidated. - The function accepts two arguments - the root state and @@ -360,6 +360,8 @@ const entries = api.util.selectInvalidatedBy(state, [ ### `invalidateTags` +A Redux action creator that can be used to manually invalidate cache tags for [automated re-fetching](../../usage/automated-refetching.mdx). + #### Signature ```ts no-transpile @@ -379,8 +381,6 @@ const invalidateTags = ( #### Description -A Redux action creator that can be used to manually invalidate cache tags for [automated re-fetching](../../usage/automated-refetching.mdx). - The action creator accepts one argument: the cache tags to be invalidated. It returns an action with those tags as a payload, and the corresponding `invalidateTags` action type for the api. Dispatching the result of this action creator will [invalidate](../../usage/automated-refetching.mdx#invalidating-cache-data) the given tags, causing queries to automatically re-fetch if they are subscribed to cache data that [provides](../../usage/automated-refetching.mdx#providing-cache-data) the corresponding tags. @@ -400,6 +400,8 @@ dispatch( ### `selectCachedArgsForQuery` +A selector function that can select arguments for currently cached queries. + #### Signature ```ts no-transpile @@ -415,8 +417,6 @@ function selectCachedArgsForQuery( #### Description -A function that can select arguments for currently cached queries. - The function accepts two arguments - the root state and diff --git a/packages/toolkit/src/query/core/buildThunks.ts b/packages/toolkit/src/query/core/buildThunks.ts index 8d9b69a2f9..9e79c3b6a7 100644 --- a/packages/toolkit/src/query/core/buildThunks.ts +++ b/packages/toolkit/src/query/core/buildThunks.ts @@ -156,7 +156,7 @@ export type PatchQueryDataThunk< PartialState, > = >( endpointName: EndpointName, - args: QueryArgFrom, + arg: QueryArgFrom, patches: readonly Patch[], updateProvided?: boolean, ) => ThunkAction @@ -166,7 +166,7 @@ export type UpdateQueryDataThunk< PartialState, > = >( endpointName: EndpointName, - args: QueryArgFrom, + arg: QueryArgFrom, updateRecipe: Recipe>, updateProvided?: boolean, ) => ThunkAction @@ -176,7 +176,7 @@ export type UpsertQueryDataThunk< PartialState, > = >( endpointName: EndpointName, - args: QueryArgFrom, + arg: QueryArgFrom, value: ResultTypeFrom, ) => ThunkAction< QueryActionCreatorResult< @@ -229,11 +229,11 @@ export function buildThunks< type State = RootState const patchQueryData: PatchQueryDataThunk = - (endpointName, args, patches, updateProvided) => (dispatch, getState) => { + (endpointName, arg, patches, updateProvided) => (dispatch, getState) => { const endpointDefinition = endpointDefinitions[endpointName] const queryCacheKey = serializeQueryArgs({ - queryArgs: args, + queryArgs: arg, endpointDefinition, endpointName, }) @@ -246,7 +246,7 @@ export function buildThunks< return } - const newValue = api.endpoints[endpointName].select(args)( + const newValue = api.endpoints[endpointName].select(arg)( // Work around TS 4.1 mismatch getState() as RootState, ) @@ -255,7 +255,7 @@ export function buildThunks< endpointDefinition.providesTags, newValue.data, undefined, - args, + arg, {}, assertTagType, ) @@ -266,11 +266,11 @@ export function buildThunks< } const updateQueryData: UpdateQueryDataThunk = - (endpointName, args, updateRecipe, updateProvided = true) => + (endpointName, arg, updateRecipe, updateProvided = true) => (dispatch, getState) => { const endpointDefinition = api.endpoints[endpointName] - const currentState = endpointDefinition.select(args)( + const currentState = endpointDefinition.select(arg)( // Work around TS 4.1 mismatch getState() as RootState, ) @@ -282,7 +282,7 @@ export function buildThunks< dispatch( api.util.patchQueryData( endpointName, - args, + arg, ret.inversePatches, updateProvided, ), @@ -317,26 +317,21 @@ export function buildThunks< } dispatch( - api.util.patchQueryData( - endpointName, - args, - ret.patches, - updateProvided, - ), + api.util.patchQueryData(endpointName, arg, ret.patches, updateProvided), ) return ret } const upsertQueryData: UpsertQueryDataThunk = - (endpointName, args, value) => (dispatch) => { + (endpointName, arg, value) => (dispatch) => { return dispatch( ( api.endpoints[endpointName] as ApiEndpointQuery< QueryDefinition, Definitions > - ).initiate(args, { + ).initiate(arg, { subscribe: false, forceRefetch: true, [forceQueryFnSymbol]: () => ({ diff --git a/packages/toolkit/src/query/core/module.ts b/packages/toolkit/src/query/core/module.ts index 06822cb039..074aa08eb8 100644 --- a/packages/toolkit/src/query/core/module.ts +++ b/packages/toolkit/src/query/core/module.ts @@ -150,7 +150,7 @@ export interface ApiModules< util: { /** * A thunk that (if dispatched) will return a specific running query, identified - * by `endpointName` and `args`. + * by `endpointName` and `arg`. * If that query is not running, dispatching the thunk will result in `undefined`. * * Can be used to await a specific query triggered in any way, @@ -160,7 +160,7 @@ export interface ApiModules< */ getRunningQueryThunk>( endpointName: EndpointName, - args: QueryArgFrom, + arg: QueryArgFrom, ): ThunkWithReturnValue< | QueryActionCreatorResult< Definitions[EndpointName] & { type: 'query' } @@ -237,9 +237,9 @@ export interface ApiModules< * * The thunk executes _synchronously_, and returns an object containing `{patches: Patch[], inversePatches: Patch[], undo: () => void}`. The `patches` and `inversePatches` are generated using Immer's [`produceWithPatches` method](https://immerjs.github.io/immer/patches). * - * This is typically used as the first step in implementing optimistic updates. The generated `inversePatches` can be used to revert the updates by calling `dispatch(patchQueryData(endpointName, args, inversePatches))`. Alternatively, the `undo` method can be called directly to achieve the same effect. + * This is typically used as the first step in implementing optimistic updates. The generated `inversePatches` can be used to revert the updates by calling `dispatch(patchQueryData(endpointName, arg, inversePatches))`. Alternatively, the `undo` method can be called directly to achieve the same effect. * - * Note that the first two arguments (`endpointName` and `args`) are used to determine which existing cache entry to update. If no existing cache entry is found, the `updateRecipe` callback will not run. + * Note that the first two arguments (`endpointName` and `arg`) are used to determine which existing cache entry to update. If no existing cache entry is found, the `updateRecipe` callback will not run. * * @example * From 3358c1371e6225b069cf68f5ecdef34141e08676 Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Mon, 14 Oct 2024 15:34:19 -0400 Subject: [PATCH 7/7] Fix Parameters headers --- .../api/created-api/api-slice-utils.mdx | 87 ++++++++++--------- 1 file changed, 47 insertions(+), 40 deletions(-) diff --git a/docs/rtk-query/api/created-api/api-slice-utils.mdx b/docs/rtk-query/api/created-api/api-slice-utils.mdx index 18d9dcbf6f..f9dd4a9786 100644 --- a/docs/rtk-query/api/created-api/api-slice-utils.mdx +++ b/docs/rtk-query/api/created-api/api-slice-utils.mdx @@ -42,11 +42,12 @@ interface PatchCollection { } ``` -- **Parameters** - - `endpointName`: a string matching an existing endpoint name - - `arg`: an argument matching that used for a previous query call, used to determine which cached dataset needs to be updated - - `updateRecipe`: an Immer `produce` callback that can apply changes to the cached state - - `updateProvided`: a boolean indicating whether the endpoint's provided tags should be re-calculated based on the updated cache. Defaults to `false`. +#### Parameters + +- `endpointName`: a string matching an existing endpoint name +- `arg`: an argument matching that used for a previous query call, used to determine which cached dataset needs to be updated +- `updateRecipe`: an Immer `produce` callback that can apply changes to the cached state +- `updateProvided`: a boolean indicating whether the endpoint's provided tags should be re-calculated based on the updated cache. Defaults to `false`. #### Description @@ -123,10 +124,11 @@ const upsertQueryData = (endpointName: string, arg: any, newEntryData: T) => ThunkAction>, PartialState, any, UnknownAction> ``` -- **Parameters** - - `endpointName`: a string matching an existing endpoint name - - `arg`: an argument matching that used for a previous query call, used to determine which cached dataset needs to be updated - - `newEntryValue`: the value to be written into the corresponding cache entry's `data` field +#### Parameters + +- `endpointName`: a string matching an existing endpoint name +- `arg`: an argument matching that used for a previous query call, used to determine which cached dataset needs to be updated +- `newEntryValue`: the value to be written into the corresponding cache entry's `data` field #### Description @@ -161,11 +163,12 @@ const patchQueryData = ( ) => ThunkAction; ``` -- **Parameters** - - `endpointName`: a string matching an existing endpoint name - - `arg`: a cache key, used to determine which cached dataset needs to be updated - - `patches`: an array of patches (or inverse patches) to apply to cached state. These would typically be obtained from the result of dispatching [`updateQueryData`](#updatequerydata) - - `updateProvided`: a boolean indicating whether the endpoint's provided tags should be re-calculated based on the updated cache. Defaults to `false`. +#### Parameters + +- `endpointName`: a string matching an existing endpoint name +- `arg`: a cache key, used to determine which cached dataset needs to be updated +- `patches`: an array of patches (or inverse patches) to apply to cached state. These would typically be obtained from the result of dispatching [`updateQueryData`](#updatequerydata) +- `updateProvided`: a boolean indicating whether the endpoint's provided tags should be re-calculated based on the updated cache. Defaults to `false`. #### Description @@ -220,11 +223,12 @@ const upsertQueryEntries = (entries: NormalizedQueryUpsertEntry[]) => PayloadAction ``` -- **Parameters** - - `entries`: an array of objects that contain the data needed to upsert individual cache entries: - - `endpointName`: the name of the endpoint, such as `"getPokemon"` - - `arg`: the full query key argument needed to identify this cache entry, such as `"pikachu"` (same as you would pass to a `useQuery` hook or `api.endpoints.someEndpoint.select()`) - - `value`: the data to be upserted into this cache entry, exactly as formatted. +#### Parameters + +- `entries`: an array of objects that contain the data needed to upsert individual cache entries: + - `endpointName`: the name of the endpoint, such as `"getPokemon"` + - `arg`: the full query key argument needed to identify this cache entry, such as `"pikachu"` (same as you would pass to a `useQuery` hook or `api.endpoints.someEndpoint.select()`) + - `value`: the data to be upserted into this cache entry, exactly as formatted. #### Description @@ -259,7 +263,7 @@ const api = createApi({ // Pre-fill the individual post entries with the results // from the list endpoint query - const entries = dispatch( + dispatch( api.util.upsertQueryEntries( posts.map((post) => ({ endpointName: 'getPost', @@ -290,13 +294,13 @@ const prefetch = (endpointName: string, arg: any, options: PrefetchOptions) => ThunkAction ``` -- **Parameters** +#### Parameters - - `endpointName`: a string matching an existing endpoint name - - `args`: a cache key, used to determine which cached dataset needs to be updated - - `options`: options to determine whether the request should be sent for a given situation: - - `ifOlderThan`: if specified, only runs the query if the difference between `new Date()` and the last`fulfilledTimeStamp` is greater than the given value (in seconds) - - `force`: if `true`, it will ignore the `ifOlderThan` value if it is set and the query will be run even if it exists in the cache. +- `endpointName`: a string matching an existing endpoint name +- `args`: a cache key, used to determine which cached dataset needs to be updated +- `options`: options to determine whether the request should be sent for a given situation: + - `ifOlderThan`: if specified, only runs the query if the difference between `new Date()` and the last`fulfilledTimeStamp` is greater than the given value (in seconds) + - `force`: if `true`, it will ignore the `ifOlderThan` value if it is set and the query will be run even if it exists in the cache. #### Description @@ -327,12 +331,13 @@ function selectInvalidatedBy( }> ``` -- **Parameters** - - `state`: the root state - - `tags`: a readonly array of invalidated tags, where the provided `TagDescription` is one of the strings provided to the [`tagTypes`](../createApi.mdx#tagtypes) property of the api. e.g. - - `[TagType]` - - `[{ type: TagType }]` - - `[{ type: TagType, id: number | string }]` +#### Parameters + +- `state`: the root state +- `tags`: a readonly array of invalidated tags, where the provided `TagDescription` is one of the strings provided to the [`tagTypes`](../createApi.mdx#tagtypes) property of the api. e.g. + - `[TagType]` + - `[{ type: TagType }]` + - `[{ type: TagType, id: number | string }]` #### Description @@ -373,11 +378,12 @@ const invalidateTags = ( }) ``` -- **Parameters** - - `tags`: an array of tags to be invalidated, where the provided `TagType` is one of the strings provided to the [`tagTypes`](../createApi.mdx#tagtypes) property of the api. e.g. - - `[TagType]` - - `[{ type: TagType }]` - - `[{ type: TagType, id: number | string }]` +#### Parameters + +- `tags`: an array of tags to be invalidated, where the provided `TagType` is one of the strings provided to the [`tagTypes`](../createApi.mdx#tagtypes) property of the api. e.g. + - `[TagType]` + - `[{ type: TagType }]` + - `[{ type: TagType, id: number | string }]` #### Description @@ -411,9 +417,10 @@ function selectCachedArgsForQuery( ): Array ``` -- **Parameters** - - `state`: the root state - - `queryName`: a string matching an existing query endpoint name +#### Parameters + +- `state`: the root state +- `queryName`: a string matching an existing query endpoint name #### Description