diff --git a/packages/toolkit/src/query/core/buildSelectors.ts b/packages/toolkit/src/query/core/buildSelectors.ts index 4ca49fc3e..73cb73ad9 100644 --- a/packages/toolkit/src/query/core/buildSelectors.ts +++ b/packages/toolkit/src/query/core/buildSelectors.ts @@ -135,6 +135,11 @@ export function buildSelectors< const selectSkippedQuery = (state: RootState) => defaultQuerySubState const selectSkippedMutation = (state: RootState) => defaultMutationSubState + const aMemoizedSelector = createSelector( + () => {}, + () => ({}), + ) + return { buildQuerySelector, buildMutationSelector, @@ -169,7 +174,7 @@ export function buildSelectors< endpointName: string, endpointDefinition: QueryDefinition, ) { - return ((queryArgs: any) => { + const memoized = aMemoizedSelector.memoize((queryArgs: any) => { const serializedArgs = serializeQueryArgs({ queryArgs, endpointDefinition, @@ -182,11 +187,15 @@ export function buildSelectors< queryArgs === skipToken ? selectSkippedQuery : selectQuerySubstate return createSelector(finalSelectQuerySubState, withRequestFlags) - }) as QueryResultSelectorFactory + }) + return Object.assign( + (arg: any) => memoized(arg), + memoized, + ) as QueryResultSelectorFactory } function buildMutationSelector() { - return ((id) => { + const memoized = aMemoizedSelector.memoize((id) => { let mutationId: string | typeof skipToken if (typeof id === 'object') { mutationId = getMutationCacheKey(id) ?? skipToken @@ -202,7 +211,11 @@ export function buildSelectors< : selectMutationSubstate return createSelector(finalSelectMutationSubstate, withRequestFlags) - }) as MutationResultSelectorFactory + }) + return Object.assign( + (arg: any) => memoized(arg), + memoized, + ) as MutationResultSelectorFactory } function selectInvalidatedBy( diff --git a/packages/toolkit/src/query/tests/buildCreateApi.test.tsx b/packages/toolkit/src/query/tests/buildCreateApi.test.tsx index 9a443c935..aebd6d1a1 100644 --- a/packages/toolkit/src/query/tests/buildCreateApi.test.tsx +++ b/packages/toolkit/src/query/tests/buildCreateApi.test.tsx @@ -163,6 +163,6 @@ describe('buildCreateApi', () => { ) // select() + selectFromResult - expect(memoize).toHaveBeenCalledTimes(8) + expect(memoize).toHaveBeenCalledTimes(4) }) }) diff --git a/packages/toolkit/src/query/tests/buildSelector.test.ts b/packages/toolkit/src/query/tests/buildSelector.test.ts new file mode 100644 index 000000000..ea6f12db0 --- /dev/null +++ b/packages/toolkit/src/query/tests/buildSelector.test.ts @@ -0,0 +1,124 @@ +import { + buildCreateApi, + coreModule, + createApi, + fetchBaseQuery, + reactHooksModule, +} from '@reduxjs/toolkit/query/react' +import { setupApiStore } from '../../tests/utils/helpers' +import { + createSelectorCreator, + lruMemoize, + type weakMapMemoize, +} from 'reselect' +import { assertCast } from '../tsHelpers' + +describe('buildSelector', () => { + interface Todo { + userId: number + id: number + title: string + completed: boolean + } + + type Todos = Array + + const exampleApi = createApi({ + baseQuery: fetchBaseQuery({ + baseUrl: 'https://jsonplaceholder.typicode.com', + }), + endpoints: (build) => ({ + getTodos: build.query({ + query: () => '/todos', + }), + getTodo: build.query({ + query: (id) => `/todos/${id}`, + }), + }), + }) + + const store = setupApiStore(exampleApi) + it('should memoize selector creation', () => { + expect(exampleApi.endpoints.getTodo.select('1')).toBe( + exampleApi.endpoints.getTodo.select('1'), + ) + + expect(exampleApi.endpoints.getTodo.select('1')).not.toBe( + exampleApi.endpoints.getTodo.select('2'), + ) + + expect( + exampleApi.endpoints.getTodo.select('1')(store.store.getState()), + ).toBe(exampleApi.endpoints.getTodo.select('1')(store.store.getState())) + + expect(exampleApi.endpoints.getTodos.select()).toBe( + exampleApi.endpoints.getTodos.select(undefined), + ) + }) + it('exposes memoize methods on select (untyped)', () => { + assertCast< + typeof exampleApi.endpoints.getTodo.select & + Omit, ''> + >(exampleApi.endpoints.getTodo.select) + + expect(exampleApi.endpoints.getTodo.select.resultsCount).toBeTypeOf( + 'function', + ) + expect(exampleApi.endpoints.getTodo.select.resetResultsCount).toBeTypeOf( + 'function', + ) + expect(exampleApi.endpoints.getTodo.select.clearCache).toBeTypeOf( + 'function', + ) + + const firstResult = exampleApi.endpoints.getTodo.select('1') + + exampleApi.endpoints.getTodo.select.clearCache() + + const secondResult = exampleApi.endpoints.getTodo.select('1') + + expect(firstResult).not.toBe(secondResult) + + expect(firstResult(store.store.getState())).not.toBe( + secondResult(store.store.getState()), + ) + }) + it('uses createSelector instance memoize', () => { + const createLruSelector = createSelectorCreator(lruMemoize) + const createApi = buildCreateApi( + coreModule({ createSelector: createLruSelector }), + reactHooksModule({ createSelector: createLruSelector }), + ) + + const exampleLruApi = createApi({ + baseQuery: fetchBaseQuery({ + baseUrl: 'https://jsonplaceholder.typicode.com', + }), + endpoints: (build) => ({ + getTodos: build.query({ + query: () => '/todos', + }), + getTodo: build.query({ + query: (id) => `/todos/${id}`, + }), + }), + }) + + expect(exampleLruApi.endpoints.getTodo.select('1')).toBe( + exampleLruApi.endpoints.getTodo.select('1'), + ) + + expect(exampleLruApi.endpoints.getTodo.select('1')).not.toBe( + exampleLruApi.endpoints.getTodo.select('2'), + ) + + const firstResult = exampleLruApi.endpoints.getTodo.select('1') + + const secondResult = exampleLruApi.endpoints.getTodo.select('2') + + const thirdResult = exampleLruApi.endpoints.getTodo.select('1') + + // cache size of 1 + expect(firstResult).not.toBe(thirdResult) + }) +})