Skip to content

Commit 8c23cd3

Browse files
authored
RTKQ: configurable structuralSharing on endpoints/queries/createApi (#1954)
* Allow for endpoints to opt out of structural sharing * Rewrite tests to expect on reference equality
1 parent d000b3b commit 8c23cd3

File tree

4 files changed

+79
-1
lines changed

4 files changed

+79
-1
lines changed

packages/toolkit/src/query/core/buildSlice.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,10 @@ export function buildSlice({
157157
(substate) => {
158158
if (substate.requestId !== meta.requestId) return
159159
substate.status = QueryStatus.fulfilled
160-
substate.data = copyWithStructuralSharing(substate.data, payload)
160+
substate.data =
161+
definitions[meta.arg.endpointName].structuralSharing ?? true
162+
? copyWithStructuralSharing(substate.data, payload)
163+
: payload
161164
delete substate.error
162165
substate.fulfilledTimeStamp = meta.fulfilledTimeStamp
163166
}

packages/toolkit/src/query/createApi.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ export function buildCreateApi<Modules extends [Module<any>, ...Module<any>[]]>(
235235
reducerPath: (options.reducerPath ?? 'api') as any,
236236
})
237237
)
238+
238239
const optionsWithDefaults = {
239240
reducerPath: 'api',
240241
serializeQueryArgs: defaultSerializeQueryArgs,

packages/toolkit/src/query/endpointDefinitions.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,19 @@ interface EndpointDefinitionWithQuery<
6363
meta: BaseQueryMeta<BaseQuery>,
6464
arg: QueryArg
6565
): ResultType | Promise<ResultType>
66+
/**
67+
* Defaults to `true`.
68+
*
69+
* Most apps should leave this setting on. The only time it can be a performance issue
70+
* is if an API returns extremely large amounts of data (e.g. 10,000 rows per request) and
71+
* you're unable to paginate it.
72+
*
73+
* For details of how this works, please see the below. When it is set to `false`,
74+
* every request will cause subscribed components to rerender, even when the data has not changed.
75+
*
76+
* @see https://redux-toolkit.js.org/api/other-exports#copywithstructuralsharing
77+
*/
78+
structuralSharing?: boolean
6679
}
6780

6881
interface EndpointDefinitionWithQueryFn<
@@ -116,6 +129,19 @@ interface EndpointDefinitionWithQueryFn<
116129
): MaybePromise<QueryReturnValue<ResultType, BaseQueryError<BaseQuery>>>
117130
query?: never
118131
transformResponse?: never
132+
/**
133+
* Defaults to `true`.
134+
*
135+
* Most apps should leave this setting on. The only time it can be a performance issue
136+
* is if an API returns extremely large amounts of data (e.g. 10,000 rows per request) and
137+
* you're unable to paginate it.
138+
*
139+
* For details of how this works, please see the below. When it is set to `false`,
140+
* every request will cause subscribed components to rerender, even when the data has not changed.
141+
*
142+
* @see https://redux-toolkit.js.org/api/other-exports#copywithstructuralsharing
143+
*/
144+
structuralSharing?: boolean
119145
}
120146

121147
export type BaseEndpointDefinition<

packages/toolkit/src/query/tests/createApi.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -763,3 +763,51 @@ test('providesTags and invalidatesTags can use baseQueryMeta', async () => {
763763

764764
expect('request' in _meta! && 'response' in _meta!).toBe(true)
765765
})
766+
767+
describe('structuralSharing flag behaviors', () => {
768+
type SuccessResponse = { value: 'success' }
769+
770+
const api = createApi({
771+
baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
772+
tagTypes: ['success'],
773+
endpoints: (build) => ({
774+
enabled: build.query<SuccessResponse, void>({
775+
query: () => '/success',
776+
}),
777+
disabled: build.query<SuccessResponse, void>({
778+
query: () => ({ url: '/success' }),
779+
structuralSharing: false,
780+
}),
781+
}),
782+
})
783+
784+
const storeRef = setupApiStore(api)
785+
786+
it('enables structural sharing for query endpoints by default', async () => {
787+
await storeRef.store.dispatch(api.endpoints.enabled.initiate())
788+
const firstRef = api.endpoints.enabled.select()(storeRef.store.getState())
789+
790+
await storeRef.store.dispatch(
791+
api.endpoints.enabled.initiate(undefined, { forceRefetch: true })
792+
)
793+
794+
const secondRef = api.endpoints.enabled.select()(storeRef.store.getState())
795+
796+
expect(firstRef.requestId).not.toEqual(secondRef.requestId)
797+
expect(firstRef.data === secondRef.data).toBeTruthy()
798+
})
799+
800+
it('allows a query endpoint to opt-out of structural sharing', async () => {
801+
await storeRef.store.dispatch(api.endpoints.disabled.initiate())
802+
const firstRef = api.endpoints.disabled.select()(storeRef.store.getState())
803+
804+
await storeRef.store.dispatch(
805+
api.endpoints.disabled.initiate(undefined, { forceRefetch: true })
806+
)
807+
808+
const secondRef = api.endpoints.disabled.select()(storeRef.store.getState())
809+
810+
expect(firstRef.requestId).not.toEqual(secondRef.requestId)
811+
expect(firstRef.data === secondRef.data).toBeFalsy()
812+
})
813+
})

0 commit comments

Comments
 (0)