UNPKG

@reduxjs/toolkit

Version:

The official, opinionated, batteries-included toolset for efficient Redux development

1,158 lines (1,049 loc) 36.6 kB
import { configureStore, createAction, createReducer } from '@reduxjs/toolkit' import type { SerializedError } from '@reduxjs/toolkit' import type { Api, MutationDefinition, QueryDefinition, } from '@reduxjs/toolkit/query' import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query' import type { FetchBaseQueryError, FetchBaseQueryMeta, } from '@reduxjs/toolkit/dist/query/fetchBaseQuery' import { ANY, expectType, expectExactType, setupApiStore, waitMs, getSerializedHeaders, } from './helpers' import { server } from './mocks/server' import { rest } from 'msw' import type { SerializeQueryArgs } from '../defaultSerializeQueryArgs' import { string } from 'yargs' import type { DefinitionsFromApi, OverrideResultType, TagTypesFromApi, } from '@reduxjs/toolkit/dist/query/endpointDefinitions' const originalEnv = process.env.NODE_ENV beforeAll(() => void ((process.env as any).NODE_ENV = 'development')) afterAll(() => void ((process.env as any).NODE_ENV = originalEnv)) let spy: jest.SpyInstance beforeAll(() => { spy = jest.spyOn(console, 'error').mockImplementation(() => {}) }) afterEach(() => { spy.mockReset() }) afterAll(() => { spy.mockRestore() }) function paginate<T>(array: T[], page_size: number, page_number: number) { // human-readable page numbers usually start with 1, so we reduce 1 in the first argument return array.slice((page_number - 1) * page_size, page_number * page_size) } test('sensible defaults', () => { const api = createApi({ baseQuery: fetchBaseQuery(), endpoints: (build) => ({ getUser: build.query<unknown, void>({ query(id) { return { url: `user/${id}` } }, }), updateUser: build.mutation<unknown, void>({ query: () => '', }), }), }) configureStore({ reducer: { [api.reducerPath]: api.reducer, }, middleware: (gDM) => gDM().concat(api.middleware), }) expect(api.reducerPath).toBe('api') expectType<'api'>(api.reducerPath) type TagTypes = typeof api extends Api<any, any, any, infer E> ? E : 'no match' expectType<TagTypes>(ANY as never) // @ts-expect-error expectType<TagTypes>(0) expect(api.endpoints.getUser.name).toBe('getUser') expect(api.endpoints.updateUser.name).toBe('updateUser') }) describe('wrong tagTypes log errors', () => { const baseQuery = jest.fn() const api = createApi({ baseQuery, tagTypes: ['User'], endpoints: (build) => ({ provideNothing: build.query<unknown, void>({ query: () => '', }), provideTypeString: build.query<unknown, void>({ query: () => '', providesTags: ['User'], }), provideTypeWithId: build.query<unknown, void>({ query: () => '', providesTags: [{ type: 'User', id: 5 }], }), provideTypeWithIdAndCallback: build.query<unknown, void>({ query: () => '', providesTags: () => [{ type: 'User', id: 5 }], }), provideWrongTypeString: build.query<unknown, void>({ query: () => '', // @ts-expect-error providesTags: ['Users'], }), provideWrongTypeWithId: build.query<unknown, void>({ query: () => '', // @ts-expect-error providesTags: [{ type: 'Users', id: 5 }], }), provideWrongTypeWithIdAndCallback: build.query<unknown, void>({ query: () => '', // @ts-expect-error providesTags: () => [{ type: 'Users', id: 5 }], }), invalidateNothing: build.query<unknown, void>({ query: () => '', }), invalidateTypeString: build.mutation<unknown, void>({ query: () => '', invalidatesTags: ['User'], }), invalidateTypeWithId: build.mutation<unknown, void>({ query: () => '', invalidatesTags: [{ type: 'User', id: 5 }], }), invalidateTypeWithIdAndCallback: build.mutation<unknown, void>({ query: () => '', invalidatesTags: () => [{ type: 'User', id: 5 }], }), invalidateWrongTypeString: build.mutation<unknown, void>({ query: () => '', // @ts-expect-error invalidatesTags: ['Users'], }), invalidateWrongTypeWithId: build.mutation<unknown, void>({ query: () => '', // @ts-expect-error invalidatesTags: [{ type: 'Users', id: 5 }], }), invalidateWrongTypeWithIdAndCallback: build.mutation<unknown, void>({ query: () => '', // @ts-expect-error invalidatesTags: () => [{ type: 'Users', id: 5 }], }), }), }) const store = configureStore({ reducer: { [api.reducerPath]: api.reducer, }, middleware: (gDM) => gDM().concat(api.middleware), }) beforeEach(() => { baseQuery.mockResolvedValue({ data: 'foo' }) }) test.each<[keyof typeof api.endpoints, boolean?]>([ ['provideNothing', false], ['provideTypeString', false], ['provideTypeWithId', false], ['provideTypeWithIdAndCallback', false], ['provideWrongTypeString', true], ['provideWrongTypeWithId', true], ['provideWrongTypeWithIdAndCallback', true], ['invalidateNothing', false], ['invalidateTypeString', false], ['invalidateTypeWithId', false], ['invalidateTypeWithIdAndCallback', false], ['invalidateWrongTypeString', true], ['invalidateWrongTypeWithId', true], ['invalidateWrongTypeWithIdAndCallback', true], ])(`endpoint %s should log an error? %s`, async (endpoint, shouldError) => { // @ts-ignore store.dispatch(api.endpoints[endpoint].initiate()) let result: { status: string } do { await waitMs(5) // @ts-ignore result = api.endpoints[endpoint].select()(store.getState()) } while (result.status === 'pending') if (shouldError) { expect(spy).toHaveBeenCalledWith( "Tag type 'Users' was used, but not specified in `tagTypes`!" ) } else { expect(spy).not.toHaveBeenCalled() } }) }) describe('endpoint definition typings', () => { const api = createApi({ baseQuery: (from: 'From'): { data: 'To' } | Promise<{ data: 'To' }> => ({ data: 'To', }), endpoints: () => ({}), tagTypes: ['typeA', 'typeB'], }) test('query: query & transformResponse types', () => { api.injectEndpoints({ endpoints: (build) => ({ query: build.query<'RetVal', 'Arg'>({ query: (x: 'Arg') => 'From' as const, transformResponse(r: 'To') { return 'RetVal' as const }, }), query1: build.query<'RetVal', 'Arg'>({ // @ts-expect-error query: (x: 'Error') => 'From' as const, transformResponse(r: 'To') { return 'RetVal' as const }, }), query2: build.query<'RetVal', 'Arg'>({ // @ts-expect-error query: (x: 'Arg') => 'Error' as const, transformResponse(r: 'To') { return 'RetVal' as const }, }), query3: build.query<'RetVal', 'Arg'>({ query: (x: 'Arg') => 'From' as const, // @ts-expect-error transformResponse(r: 'Error') { return 'RetVal' as const }, }), query4: build.query<'RetVal', 'Arg'>({ query: (x: 'Arg') => 'From' as const, // @ts-expect-error transformResponse(r: 'To') { return 'Error' as const }, }), queryInference1: build.query<'RetVal', 'Arg'>({ query: (x) => { expectType<'Arg'>(x) return 'From' }, transformResponse(r) { expectType<'To'>(r) return 'RetVal' }, }), queryInference2: (() => { const query = build.query({ query: (x: 'Arg') => 'From' as const, transformResponse(r: 'To') { return 'RetVal' as const }, }) expectType<QueryDefinition<'Arg', any, any, 'RetVal'>>(query) return query })(), }), }) }) test('mutation: query & transformResponse types', () => { api.injectEndpoints({ endpoints: (build) => ({ query: build.mutation<'RetVal', 'Arg'>({ query: (x: 'Arg') => 'From' as const, transformResponse(r: 'To') { return 'RetVal' as const }, }), query1: build.mutation<'RetVal', 'Arg'>({ // @ts-expect-error query: (x: 'Error') => 'From' as const, transformResponse(r: 'To') { return 'RetVal' as const }, }), query2: build.mutation<'RetVal', 'Arg'>({ // @ts-expect-error query: (x: 'Arg') => 'Error' as const, transformResponse(r: 'To') { return 'RetVal' as const }, }), query3: build.mutation<'RetVal', 'Arg'>({ query: (x: 'Arg') => 'From' as const, // @ts-expect-error transformResponse(r: 'Error') { return 'RetVal' as const }, }), query4: build.mutation<'RetVal', 'Arg'>({ query: (x: 'Arg') => 'From' as const, // @ts-expect-error transformResponse(r: 'To') { return 'Error' as const }, }), mutationInference1: build.mutation<'RetVal', 'Arg'>({ query: (x) => { expectType<'Arg'>(x) return 'From' }, transformResponse(r) { expectType<'To'>(r) return 'RetVal' }, }), mutationInference2: (() => { const query = build.mutation({ query: (x: 'Arg') => 'From' as const, transformResponse(r: 'To') { return 'RetVal' as const }, }) expectType<MutationDefinition<'Arg', any, any, 'RetVal'>>(query) return query })(), }), }) }) describe('enhancing endpoint definitions', () => { const baseQuery = jest.fn((x: string) => ({ data: 'success' })) const commonBaseQueryApi = { dispatch: expect.any(Function), endpoint: expect.any(String), abort: expect.any(Function), extra: undefined, forced: expect.any(Boolean), getState: expect.any(Function), signal: expect.any(Object), type: expect.any(String), } beforeEach(() => { baseQuery.mockClear() }) function getNewApi() { return createApi({ baseQuery, tagTypes: ['old'], endpoints: (build) => ({ query1: build.query<'out1', 'in1'>({ query: (id) => `${id}` }), query2: build.query<'out2', 'in2'>({ query: (id) => `${id}` }), mutation1: build.mutation<'out1', 'in1'>({ query: (id) => `${id}` }), mutation2: build.mutation<'out2', 'in2'>({ query: (id) => `${id}` }), }), }) } let api = getNewApi() beforeEach(() => { api = getNewApi() }) test('pre-modification behaviour', async () => { const storeRef = setupApiStore(api, undefined, { withoutTestLifecycles: true, }) storeRef.store.dispatch(api.endpoints.query1.initiate('in1')) storeRef.store.dispatch(api.endpoints.query2.initiate('in2')) storeRef.store.dispatch(api.endpoints.mutation1.initiate('in1')) storeRef.store.dispatch(api.endpoints.mutation2.initiate('in2')) expect(baseQuery.mock.calls).toEqual([ [ 'in1', { dispatch: expect.any(Function), endpoint: expect.any(String), getState: expect.any(Function), signal: expect.any(Object), abort: expect.any(Function), forced: expect.any(Boolean), type: expect.any(String), }, undefined, ], [ 'in2', { dispatch: expect.any(Function), endpoint: expect.any(String), getState: expect.any(Function), signal: expect.any(Object), abort: expect.any(Function), forced: expect.any(Boolean), type: expect.any(String), }, undefined, ], [ 'in1', { dispatch: expect.any(Function), endpoint: expect.any(String), getState: expect.any(Function), signal: expect.any(Object), abort: expect.any(Function), // forced: undefined, type: expect.any(String), }, undefined, ], [ 'in2', { dispatch: expect.any(Function), endpoint: expect.any(String), getState: expect.any(Function), signal: expect.any(Object), abort: expect.any(Function), // forced: undefined, type: expect.any(String), }, undefined, ], ]) }) test('warn on wrong tagType', async () => { const storeRef = setupApiStore(api, undefined, { withoutTestLifecycles: true, }) // only type-test this part if (2 > 1) { api.enhanceEndpoints({ endpoints: { query1: { // @ts-expect-error providesTags: ['new'], }, query2: { // @ts-expect-error providesTags: ['missing'], }, }, }) } const enhanced = api.enhanceEndpoints({ addTagTypes: ['new'], endpoints: { query1: { providesTags: ['new'], }, query2: { // @ts-expect-error providesTags: ['missing'], }, }, }) storeRef.store.dispatch(api.endpoints.query1.initiate('in1')) await waitMs(1) expect(spy).not.toHaveBeenCalled() storeRef.store.dispatch(api.endpoints.query2.initiate('in2')) await waitMs(1) expect(spy).toHaveBeenCalledWith( "Tag type 'missing' was used, but not specified in `tagTypes`!" ) // only type-test this part if (2 > 1) { enhanced.enhanceEndpoints({ endpoints: { query1: { // returned `enhanced` api contains "new" enitityType providesTags: ['new'], }, query2: { // @ts-expect-error providesTags: ['missing'], }, }, }) } }) test('modify', () => { const storeRef = setupApiStore(api, undefined, { withoutTestLifecycles: true, }) api.enhanceEndpoints({ endpoints: { query1: { query: (x) => { expectExactType('in1' as const)(x) return 'modified1' }, }, query2(definition) { definition.query = (x) => { expectExactType('in2' as const)(x) return 'modified2' } }, mutation1: { query: (x) => { expectExactType('in1' as const)(x) return 'modified1' }, }, mutation2(definition) { definition.query = (x) => { expectExactType('in2' as const)(x) return 'modified2' } }, // @ts-expect-error nonExisting: {}, }, }) storeRef.store.dispatch(api.endpoints.query1.initiate('in1')) storeRef.store.dispatch(api.endpoints.query2.initiate('in2')) storeRef.store.dispatch(api.endpoints.mutation1.initiate('in1')) storeRef.store.dispatch(api.endpoints.mutation2.initiate('in2')) expect(baseQuery.mock.calls).toEqual([ ['modified1', commonBaseQueryApi, undefined], ['modified2', commonBaseQueryApi, undefined], ['modified1', { ...commonBaseQueryApi, forced: undefined }, undefined], ['modified2', { ...commonBaseQueryApi, forced: undefined }, undefined], ]) }) test('updated transform response types', async () => { const baseApi = createApi({ baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }), tagTypes: ['old'], endpoints: (build) => ({ query1: build.query<'out1', void>({ query: () => 'success' }), mutation1: build.mutation<'out1', void>({ query: () => 'success' }), }), }) type Transformed = { value: string } type Definitions = DefinitionsFromApi<typeof api> type TagTypes = TagTypesFromApi<typeof api> type Q1Definition = OverrideResultType<Definitions['query1'], Transformed> type M1Definition = OverrideResultType< Definitions['mutation1'], Transformed > type UpdatedDefitions = Omit<Definitions, 'query1' | 'mutation1'> & { query1: Q1Definition mutation1: M1Definition } const enhancedApi = baseApi.enhanceEndpoints<TagTypes, UpdatedDefitions>({ endpoints: { query1: { transformResponse: (a, b, c) => ({ value: 'transformed', }), }, mutation1: { transformResponse: (a, b, c) => ({ value: 'transformed', }), }, }, }) const storeRef = setupApiStore(enhancedApi, undefined, { withoutTestLifecycles: true, }) const queryResponse = await storeRef.store.dispatch( enhancedApi.endpoints.query1.initiate() ) expect(queryResponse.data).toEqual({ value: 'transformed' }) expectType<Transformed | undefined>(queryResponse.data) const mutationResponse = await storeRef.store.dispatch( enhancedApi.endpoints.mutation1.initiate() ) expectType< { data: Transformed } | { error: FetchBaseQueryError | SerializedError } >(mutationResponse) expect('data' in mutationResponse && mutationResponse.data).toEqual({ value: 'transformed', }) }) }) }) describe('additional transformResponse behaviors', () => { type SuccessResponse = { value: 'success' } type EchoResponseData = { banana: 'bread' } type ErrorResponse = { value: 'error' } const api = createApi({ baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }), endpoints: (build) => ({ echo: build.mutation({ query: () => ({ method: 'PUT', url: '/echo' }), }), mutation: build.mutation({ query: () => ({ url: '/echo', method: 'POST', body: { nested: { banana: 'bread' } }, }), transformResponse: (response: { body: { nested: EchoResponseData } }) => response.body.nested, }), mutationWithError: build.mutation({ query: () => ({ url: '/error', method: 'POST', }), transformErrorResponse: (response) => { const data = response.data as ErrorResponse return data.value }, }), mutationWithMeta: build.mutation({ query: () => ({ url: '/echo', method: 'POST', body: { nested: { banana: 'bread' } }, }), transformResponse: ( response: { body: { nested: EchoResponseData } }, meta ) => { return { ...response.body.nested, meta: { request: { headers: getSerializedHeaders(meta?.request.headers) }, response: { headers: getSerializedHeaders(meta?.response?.headers), }, }, } }, }), query: build.query<SuccessResponse & EchoResponseData, void>({ query: () => '/success', transformResponse: async (response: SuccessResponse) => { const res = await fetch('https://example.com/echo', { method: 'POST', body: JSON.stringify({ banana: 'bread' }), }).then((res) => res.json()) const additionalData = JSON.parse(res.body) as EchoResponseData return { ...response, ...additionalData } }, }), queryWithMeta: build.query<SuccessResponse, void>({ query: () => '/success', transformResponse: async (response: SuccessResponse, meta) => { return { ...response, meta: { request: { headers: getSerializedHeaders(meta?.request.headers) }, response: { headers: getSerializedHeaders(meta?.response?.headers), }, }, } }, }), }), }) const storeRef = setupApiStore(api) test('transformResponse handles an async transformation and returns the merged data (query)', async () => { const result = await storeRef.store.dispatch(api.endpoints.query.initiate()) expect(result.data).toEqual({ value: 'success', banana: 'bread' }) }) test('transformResponse transforms a response from a mutation', async () => { const result = await storeRef.store.dispatch( api.endpoints.mutation.initiate({}) ) expect('data' in result && result.data).toEqual({ banana: 'bread' }) }) test('transformResponse transforms a response from a mutation with an error', async () => { const result = await storeRef.store.dispatch( api.endpoints.mutationWithError.initiate({}) ) expect('error' in result && result.error).toEqual('error') }) test('transformResponse can inject baseQuery meta into the end result from a mutation', async () => { const result = await storeRef.store.dispatch( api.endpoints.mutationWithMeta.initiate({}) ) expect('data' in result && result.data).toEqual({ banana: 'bread', meta: { request: { headers: { 'content-type': 'application/json', }, }, response: { headers: { 'content-type': 'application/json', 'x-powered-by': 'msw', }, }, }, }) }) test('transformResponse can inject baseQuery meta into the end result from a query', async () => { const result = await storeRef.store.dispatch( api.endpoints.queryWithMeta.initiate() ) expect(result.data).toEqual({ value: 'success', meta: { request: { headers: {}, }, response: { headers: { 'content-type': 'application/json', 'x-powered-by': 'msw', }, }, }, }) }) }) describe('query endpoint lifecycles - onStart, onSuccess, onError', () => { const initialState = { count: null as null | number, } const setCount = createAction<number>('setCount') const testReducer = createReducer(initialState, (builder) => { builder.addCase(setCount, (state, action) => { state.count = action.payload }) }) type SuccessResponse = { value: 'success' } const api = createApi({ baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }), endpoints: (build) => ({ echo: build.mutation({ query: () => ({ method: 'PUT', url: '/echo' }), }), query: build.query<SuccessResponse, void>({ query: () => '/success', async onQueryStarted(_, api) { api.dispatch(setCount(0)) try { await api.queryFulfilled api.dispatch(setCount(1)) } catch { api.dispatch(setCount(-1)) } }, }), mutation: build.mutation<SuccessResponse, void>({ query: () => ({ url: '/success', method: 'POST' }), async onQueryStarted(_, api) { api.dispatch(setCount(0)) try { await api.queryFulfilled api.dispatch(setCount(1)) } catch { api.dispatch(setCount(-1)) } }, }), }), }) const storeRef = setupApiStore(api, { testReducer }) test('query lifecycle events fire properly', async () => { // We intentionally fail the first request so we can test all lifecycles server.use( rest.get('https://example.com/success', (_, res, ctx) => res.once(ctx.status(500), ctx.json({ value: 'failed' })) ) ) expect(storeRef.store.getState().testReducer.count).toBe(null) const failAttempt = storeRef.store.dispatch(api.endpoints.query.initiate()) expect(storeRef.store.getState().testReducer.count).toBe(0) await failAttempt await waitMs(10) expect(storeRef.store.getState().testReducer.count).toBe(-1) const successAttempt = storeRef.store.dispatch( api.endpoints.query.initiate() ) expect(storeRef.store.getState().testReducer.count).toBe(0) await successAttempt await waitMs(10) expect(storeRef.store.getState().testReducer.count).toBe(1) }) test('mutation lifecycle events fire properly', async () => { // We intentionally fail the first request so we can test all lifecycles server.use( rest.post('https://example.com/success', (_, res, ctx) => res.once(ctx.status(500), ctx.json({ value: 'failed' })) ) ) expect(storeRef.store.getState().testReducer.count).toBe(null) const failAttempt = storeRef.store.dispatch( api.endpoints.mutation.initiate() ) expect(storeRef.store.getState().testReducer.count).toBe(0) await failAttempt expect(storeRef.store.getState().testReducer.count).toBe(-1) const successAttempt = storeRef.store.dispatch( api.endpoints.mutation.initiate() ) expect(storeRef.store.getState().testReducer.count).toBe(0) await successAttempt expect(storeRef.store.getState().testReducer.count).toBe(1) }) }) test('providesTags and invalidatesTags can use baseQueryMeta', async () => { let _meta: FetchBaseQueryMeta | undefined type SuccessResponse = { value: 'success' } const api = createApi({ baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }), tagTypes: ['success'], endpoints: (build) => ({ query: build.query<SuccessResponse, void>({ query: () => '/success', providesTags: (_result, _error, _arg, meta) => { _meta = meta return ['success'] }, }), mutation: build.mutation<SuccessResponse, void>({ query: () => ({ url: '/success', method: 'POST' }), invalidatesTags: (_result, _error, _arg, meta) => { _meta = meta return ['success'] }, }), }), }) const storeRef = setupApiStore(api, undefined, { withoutTestLifecycles: true, }) await storeRef.store.dispatch(api.endpoints.query.initiate()) expect('request' in _meta! && 'response' in _meta!).toBe(true) _meta = undefined await storeRef.store.dispatch(api.endpoints.mutation.initiate()) expect('request' in _meta! && 'response' in _meta!).toBe(true) }) describe('structuralSharing flag behaviors', () => { type SuccessResponse = { value: 'success' } const api = createApi({ baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }), tagTypes: ['success'], endpoints: (build) => ({ enabled: build.query<SuccessResponse, void>({ query: () => '/success', }), disabled: build.query<SuccessResponse, void>({ query: () => ({ url: '/success' }), structuralSharing: false, }), }), }) const storeRef = setupApiStore(api) it('enables structural sharing for query endpoints by default', async () => { await storeRef.store.dispatch(api.endpoints.enabled.initiate()) const firstRef = api.endpoints.enabled.select()(storeRef.store.getState()) await storeRef.store.dispatch( api.endpoints.enabled.initiate(undefined, { forceRefetch: true }) ) const secondRef = api.endpoints.enabled.select()(storeRef.store.getState()) expect(firstRef.requestId).not.toEqual(secondRef.requestId) expect(firstRef.data === secondRef.data).toBeTruthy() }) it('allows a query endpoint to opt-out of structural sharing', async () => { await storeRef.store.dispatch(api.endpoints.disabled.initiate()) const firstRef = api.endpoints.disabled.select()(storeRef.store.getState()) await storeRef.store.dispatch( api.endpoints.disabled.initiate(undefined, { forceRefetch: true }) ) const secondRef = api.endpoints.disabled.select()(storeRef.store.getState()) expect(firstRef.requestId).not.toEqual(secondRef.requestId) expect(firstRef.data === secondRef.data).toBeFalsy() }) }) describe('custom serializeQueryArgs per endpoint', () => { const customArgsSerializer: SerializeQueryArgs<number> = ({ endpointName, queryArgs, }) => `${endpointName}-${queryArgs}` type SuccessResponse = { value: 'success' } const serializer1 = jest.fn(customArgsSerializer) interface MyApiClient { fetchPost: (id: string) => Promise<SuccessResponse> } const dummyClient: MyApiClient = { async fetchPost(id) { return { value: 'success' } }, } const api = createApi({ baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }), serializeQueryArgs: ({ endpointName, queryArgs }) => `base-${endpointName}-${queryArgs}`, endpoints: (build) => ({ queryWithNoSerializer: build.query<SuccessResponse, number>({ query: (arg) => `${arg}`, }), queryWithCustomSerializer: build.query<SuccessResponse, number>({ query: (arg) => `${arg}`, serializeQueryArgs: serializer1, }), queryWithCustomObjectSerializer: build.query< SuccessResponse, { id: number; client: MyApiClient } >({ query: (arg) => `${arg.id}`, serializeQueryArgs: ({ endpointDefinition, endpointName, queryArgs, }) => { const { id } = queryArgs return { id } }, }), queryWithCustomNumberSerializer: build.query< SuccessResponse, { id: number; client: MyApiClient } >({ query: (arg) => `${arg.id}`, serializeQueryArgs: ({ endpointDefinition, endpointName, queryArgs, }) => { const { id } = queryArgs return id }, }), listItems: build.query<string[], number>({ query: (pageNumber) => `/listItems?page=${pageNumber}`, serializeQueryArgs: ({ endpointName }) => { return endpointName }, merge: (currentCache, newItems) => { currentCache.push(...newItems) }, forceRefetch({ currentArg, previousArg }) { return currentArg !== previousArg }, }), listItems2: build.query<{ items: string[]; meta?: any }, number>({ query: (pageNumber) => `/listItems2?page=${pageNumber}`, serializeQueryArgs: ({ endpointName }) => { return endpointName }, transformResponse(items: string[]) { return { items } }, merge: (currentCache, newData, meta) => { currentCache.items.push(...newData.items) currentCache.meta = meta }, forceRefetch({ currentArg, previousArg }) { return currentArg !== previousArg }, }), }), }) const storeRef = setupApiStore(api) it('Works via createApi', async () => { await storeRef.store.dispatch( api.endpoints.queryWithNoSerializer.initiate(99) ) expect(serializer1).toHaveBeenCalledTimes(0) await storeRef.store.dispatch( api.endpoints.queryWithCustomSerializer.initiate(42) ) expect(serializer1).toHaveBeenCalled() expect( storeRef.store.getState().api.queries['base-queryWithNoSerializer-99'] ).toBeTruthy() expect( storeRef.store.getState().api.queries['queryWithCustomSerializer-42'] ).toBeTruthy() }) const serializer2 = jest.fn(customArgsSerializer) const injectedApi = api.injectEndpoints({ endpoints: (build) => ({ injectedQueryWithCustomSerializer: build.query<SuccessResponse, number>({ query: (arg) => `${arg}`, serializeQueryArgs: serializer2, }), }), }) it('Works via injectEndpoints', async () => { expect(serializer2).toHaveBeenCalledTimes(0) await storeRef.store.dispatch( injectedApi.endpoints.injectedQueryWithCustomSerializer.initiate(5) ) expect(serializer2).toHaveBeenCalled() expect( storeRef.store.getState().api.queries[ 'injectedQueryWithCustomSerializer-5' ] ).toBeTruthy() }) test('Serializes a returned object for query args', async () => { await storeRef.store.dispatch( api.endpoints.queryWithCustomObjectSerializer.initiate({ id: 42, client: dummyClient, }) ) expect( storeRef.store.getState().api.queries[ 'queryWithCustomObjectSerializer({"id":42})' ] ).toBeTruthy() }) test('Serializes a returned primitive for query args', async () => { await storeRef.store.dispatch( api.endpoints.queryWithCustomNumberSerializer.initiate({ id: 42, client: dummyClient, }) ) expect( storeRef.store.getState().api.queries[ 'queryWithCustomNumberSerializer(42)' ] ).toBeTruthy() }) test('serializeQueryArgs + merge allows refetching as args change with same cache key', async () => { const allItems = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'i'] const PAGE_SIZE = 3 server.use( rest.get('https://example.com/listItems', (req, res, ctx) => { const pageString = req.url.searchParams.get('page') const pageNum = parseInt(pageString || '0') const results = paginate(allItems, PAGE_SIZE, pageNum) return res(ctx.json(results)) }) ) // Page number shouldn't matter here, because the cache key ignores that. // We just need to select the only cache entry. const selectListItems = api.endpoints.listItems.select(0) await storeRef.store.dispatch(api.endpoints.listItems.initiate(1)) const initialEntry = selectListItems(storeRef.store.getState()) expect(initialEntry.data).toEqual(['a', 'b', 'c']) await storeRef.store.dispatch(api.endpoints.listItems.initiate(2)) const updatedEntry = selectListItems(storeRef.store.getState()) expect(updatedEntry.data).toEqual(['a', 'b', 'c', 'd', 'e', 'f']) }) test('merge receives a meta object as an argument', async () => { const allItems = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'i'] const PAGE_SIZE = 3 server.use( rest.get('https://example.com/listItems2', (req, res, ctx) => { const pageString = req.url.searchParams.get('page') const pageNum = parseInt(pageString || '0') const results = paginate(allItems, PAGE_SIZE, pageNum) return res(ctx.json(results)) }) ) const selectListItems = api.endpoints.listItems2.select(0) await storeRef.store.dispatch(api.endpoints.listItems2.initiate(1)) await storeRef.store.dispatch(api.endpoints.listItems2.initiate(2)) const cacheEntry = selectListItems(storeRef.store.getState()) // Should have passed along the third arg from `merge` containing these fields expect(cacheEntry.data?.meta).toEqual({ requestId: expect.any(String), fulfilledTimeStamp: expect.any(Number), arg: 2, baseQueryMeta: expect.any(Object), }) }) })