diff --git a/packages/toolkit/src/query/core/buildSlice.ts b/packages/toolkit/src/query/core/buildSlice.ts index 3c8fd81fcd..8e1c9e05d1 100644 --- a/packages/toolkit/src/query/core/buildSlice.ts +++ b/packages/toolkit/src/query/core/buildSlice.ts @@ -157,7 +157,10 @@ export function buildSlice({ (substate) => { if (substate.requestId !== meta.requestId) return substate.status = QueryStatus.fulfilled - substate.data = copyWithStructuralSharing(substate.data, payload) + substate.data = + definitions[meta.arg.endpointName].structuralSharing ?? true + ? copyWithStructuralSharing(substate.data, payload) + : payload delete substate.error substate.fulfilledTimeStamp = meta.fulfilledTimeStamp } diff --git a/packages/toolkit/src/query/createApi.ts b/packages/toolkit/src/query/createApi.ts index d846cd1c29..e70e02281b 100644 --- a/packages/toolkit/src/query/createApi.ts +++ b/packages/toolkit/src/query/createApi.ts @@ -235,6 +235,7 @@ export function buildCreateApi, ...Module[]]>( reducerPath: (options.reducerPath ?? 'api') as any, }) ) + const optionsWithDefaults = { reducerPath: 'api', serializeQueryArgs: defaultSerializeQueryArgs, diff --git a/packages/toolkit/src/query/endpointDefinitions.ts b/packages/toolkit/src/query/endpointDefinitions.ts index d7af5dacaa..44ce1a41b1 100644 --- a/packages/toolkit/src/query/endpointDefinitions.ts +++ b/packages/toolkit/src/query/endpointDefinitions.ts @@ -63,6 +63,19 @@ interface EndpointDefinitionWithQuery< meta: BaseQueryMeta, arg: QueryArg ): ResultType | Promise + /** + * 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. + * + * For details of how this works, please see the below. When it is set to `false`, + * every request will cause subscribed components to rerender, even when the data has not changed. + * + * @see https://redux-toolkit.js.org/api/other-exports#copywithstructuralsharing + */ + structuralSharing?: boolean } interface EndpointDefinitionWithQueryFn< @@ -116,6 +129,19 @@ interface EndpointDefinitionWithQueryFn< ): MaybePromise>> query?: never transformResponse?: never + /** + * 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. + * + * For details of how this works, please see the below. When it is set to `false`, + * every request will cause subscribed components to rerender, even when the data has not changed. + * + * @see https://redux-toolkit.js.org/api/other-exports#copywithstructuralsharing + */ + structuralSharing?: boolean } export type BaseEndpointDefinition< diff --git a/packages/toolkit/src/query/tests/createApi.test.ts b/packages/toolkit/src/query/tests/createApi.test.ts index 8ccc480657..a81c22381b 100644 --- a/packages/toolkit/src/query/tests/createApi.test.ts +++ b/packages/toolkit/src/query/tests/createApi.test.ts @@ -763,3 +763,51 @@ test('providesTags and invalidatesTags can use baseQueryMeta', async () => { 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({ + query: () => '/success', + }), + disabled: build.query({ + 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() + }) +})