diff --git a/packages/toolkit/src/query/core/buildInitiate.ts b/packages/toolkit/src/query/core/buildInitiate.ts index c6dc94ad6f..157124763f 100644 --- a/packages/toolkit/src/query/core/buildInitiate.ts +++ b/packages/toolkit/src/query/core/buildInitiate.ts @@ -265,7 +265,7 @@ Features like automatic cache collection, automatic refetching etc. will not be subscribe = true, forceRefetch, subscriptionOptions, - structuralSharing = true, + structuralSharing, } = {} ) => (dispatch, getState) => { diff --git a/packages/toolkit/src/query/core/buildSlice.ts b/packages/toolkit/src/query/core/buildSlice.ts index b295763de7..01f30bfb27 100644 --- a/packages/toolkit/src/query/core/buildSlice.ts +++ b/packages/toolkit/src/query/core/buildSlice.ts @@ -92,7 +92,7 @@ export function buildSlice({ apiUid, extractRehydrationInfo, hasRehydrationInfo, - structuralSharing, + structuralSharing: globalStructuralSharing, }, assertTagType, config, @@ -156,10 +156,16 @@ export function buildSlice({ draft, meta.arg.queryCacheKey, (substate) => { + const endpointStructuralSharing = + definitions[meta.arg.endpointName].structuralSharing + const argStructuralSharing = meta.arg.structuralSharing + if (substate.requestId !== meta.requestId) return substate.status = QueryStatus.fulfilled substate.data = - structuralSharing && meta.arg.structuralSharing + argStructuralSharing ?? + endpointStructuralSharing ?? + globalStructuralSharing ? copyWithStructuralSharing(substate.data, payload) : payload delete substate.error diff --git a/packages/toolkit/src/query/react/buildHooks.ts b/packages/toolkit/src/query/react/buildHooks.ts index 15e2977937..3fe29a34f9 100644 --- a/packages/toolkit/src/query/react/buildHooks.ts +++ b/packages/toolkit/src/query/react/buildHooks.ts @@ -142,6 +142,16 @@ interface UseQuerySubscriptionOptions extends SubscriptionOptions { * If you specify this option alongside `skip: true`, this **will not be evaluated** until `skip` is false. */ refetchOnMountOrArgChange?: boolean | number + /** + * Defaults to `true`. + * + * Most apps should leave this setting on. The only time it can be a performance issue + * is if an API returns extremely large amounts of data (e.g. 10,000 rows per request) and + * you're unable to paginate it. + * + * @see https://redux-toolkit.js.org/api/other-exports#copywithstructuralsharing + */ + structuralSharing?: boolean } /** @@ -611,6 +621,7 @@ export function buildHooks({ refetchOnMountOrArgChange, skip = false, pollingInterval = 0, + structuralSharing, } = {} ) => { const { initiate } = api.endpoints[name] as ApiEndpointQuery< @@ -668,6 +679,7 @@ export function buildHooks({ initiate(stableArg, { subscriptionOptions: stableSubscriptionOptions, forceRefetch: refetchOnMountOrArgChange, + structuralSharing, }) ) promiseRef.current = promise diff --git a/packages/toolkit/src/query/tests/createApi.test.ts b/packages/toolkit/src/query/tests/createApi.test.ts index 8ccc480657..485261d44e 100644 --- a/packages/toolkit/src/query/tests/createApi.test.ts +++ b/packages/toolkit/src/query/tests/createApi.test.ts @@ -17,6 +17,7 @@ import { } from './helpers' import { server } from './mocks/server' import { rest } from 'msw' +import * as utils from '../utils' const originalEnv = process.env.NODE_ENV beforeAll(() => void ((process.env as any).NODE_ENV = 'development')) @@ -763,3 +764,62 @@ test('providesTags and invalidatesTags can use baseQueryMeta', async () => { expect('request' in _meta! && 'response' in _meta!).toBe(true) }) + +describe('strucutralSharing flag behaviors', () => { + const mockCopyFn = jest.spyOn(utils, 'copyWithStructuralSharing') + + beforeEach(() => { + mockCopyFn.mockClear() + }) + + type SuccessResponse = { value: 'success' } + + const apiSuccessResponse: SuccessResponse = { value: 'success' } + + const api = createApi({ + baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }), + tagTypes: ['success'], + endpoints: (build) => ({ + enabled: build.query({ + query: () => '/success', + }), + disabled: build.query({ + query: () => ({ url: '/success' }), + structuralSharing: false, + }), + }), + }) + + const storeRef = setupApiStore(api) + + it('enables structural sharing for query endpoints by default', async () => { + const result = await storeRef.store.dispatch( + api.endpoints.enabled.initiate() + ) + expect(mockCopyFn).toHaveBeenCalledTimes(1) + expect(result.data).toMatchObject(apiSuccessResponse) + }) + it('allows a query endpoint to opt-out of structural sharing', async () => { + const result = await storeRef.store.dispatch( + api.endpoints.disabled.initiate() + ) + expect(mockCopyFn).toHaveBeenCalledTimes(0) + expect(result.data).toMatchObject(apiSuccessResponse) + }) + it('allows initiate to override endpoint and global settings and disable at the call site level', async () => { + // global flag is enabled, endpoint is also enabled by default + const result = await storeRef.store.dispatch( + api.endpoints.enabled.initiate(undefined, { structuralSharing: false }) + ) + expect(mockCopyFn).toHaveBeenCalledTimes(0) + expect(result.data).toMatchObject(apiSuccessResponse) + }) + it('allows initiate to override the endpoint flag and enable sharing at the call site', async () => { + // global flag is enabled, endpoint is disabled + const result = await storeRef.store.dispatch( + api.endpoints.disabled.initiate(undefined, { structuralSharing: true }) + ) + expect(mockCopyFn).toHaveBeenCalledTimes(1) + expect(result.data).toMatchObject(apiSuccessResponse) + }) +})