Skip to content

Commit f9ebf9a

Browse files
authored
feat: allow custom function for structuralSharing (#3868)
1 parent f82e6e0 commit f9ebf9a

File tree

5 files changed

+42
-4
lines changed

5 files changed

+42
-4
lines changed

docs/guides/important-defaults.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,11 @@ If you see a refetch that you are not expecting, it is likely because you just f
3030
3131
- Query results by default are **structurally shared to detect if data has actually changed** and if not, **the data reference remains unchanged** to better help with value stabilization with regards to useMemo and useCallback. If this concept sounds foreign, then don't worry about it! 99.9% of the time you will not need to disable this and it makes your app more performant at zero cost to you.
3232

33-
> Structural sharing only works with JSON-compatible values, any other value types will always be considered as changed. If you are seeing performance issues because of large responses for example, you can disable this feature with the `config.structuralSharing` flag. If you are dealing with non-JSON compatible values in your query responses and still want to detect if data has changed or not, you can define a data compare function with `config.isDataEqual`.
33+
> Structural sharing only works with JSON-compatible values, any other value types will always be considered as changed. If you are seeing performance issues because of large responses for example, you can disable this feature with the `config.structuralSharing` flag. If you are dealing with non-JSON compatible values in your query responses and still want to detect if data has changed or not, you can define a data compare function with `config.isDataEqual` or provide your own custom function as `config.structuralSharing` to compute a value from the old and new responses, retaining references as required.
3434
3535
## Further Reading
3636

3737
Have a look at the following articles from our Community Resources for further explanations of the defaults:
3838

3939
- [Practical React Query](../community/tkdodos-blog#1-practical-react-query)
4040
- [React Query as a State Manager](../community/tkdodos-blog#10-react-query-as-a-state-manager)
41-

docs/reference/useQuery.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,10 +178,11 @@ const result = useQuery({
178178
- `isDataEqual: (oldData: TData | undefined, newData: TData) => boolean`
179179
- Optional
180180
- This function should return boolean indicating whether to use previous `data` (`true`) or new data (`false`) as a resolved data for the query.
181-
- `structuralSharing: boolean`
181+
- `structuralSharing: boolean | ((oldData: TData | undefined, newData: TData) => TData)`
182182
- Optional
183183
- Defaults to `true`
184184
- If set to `false`, structural sharing between query results will be disabled.
185+
- If set to a function, the old and new data values will be passed through this function, which should combine them into resolved data for the query. This way, you can retain references from the old data to improve performance even when that data contains non-serializable values.
185186
- `useErrorBoundary: undefined | boolean | (error: TError, query: Query) => boolean`
186187
- Defaults to the global query config's `useErrorBoundary` value, which is `undefined`
187188
- Set this to `true` if you want errors to be thrown in the render phase and propagate to the nearest error boundary

packages/query-core/src/tests/queryClient.test.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
QueryClient,
1414
QueryFunction,
1515
QueryObserver,
16+
QueryObserverOptions,
1617
} from '..'
1718
import { focusManager, onlineManager } from '..'
1819

@@ -357,6 +358,38 @@ describe('queryClient', () => {
357358
expect(queryCache.find(key)!.state.data).toBe(newData)
358359
})
359360

361+
test('should apply a custom structuralSharing function when provided', () => {
362+
const key = queryKey()
363+
364+
const queryObserverOptions = {
365+
structuralSharing: (
366+
prevData: { value: Date } | undefined,
367+
newData: { value: Date },
368+
) => {
369+
if (!prevData) {
370+
return newData
371+
}
372+
return newData.value.getTime() === prevData.value.getTime()
373+
? prevData
374+
: newData
375+
},
376+
} as QueryObserverOptions
377+
378+
queryClient.setDefaultOptions({ queries: queryObserverOptions })
379+
380+
const oldData = { value: new Date(2022, 6, 19) }
381+
const newData = { value: new Date(2022, 6, 19) }
382+
queryClient.setQueryData(key, oldData)
383+
queryClient.setQueryData(key, newData)
384+
385+
expect(queryCache.find(key)!.state.data).toBe(oldData)
386+
387+
const distinctData = { value: new Date(2021, 11, 25) }
388+
queryClient.setQueryData(key, distinctData)
389+
390+
expect(queryCache.find(key)!.state.data).toBe(distinctData)
391+
})
392+
360393
test('should not set isFetching to false', async () => {
361394
const key = queryKey()
362395
queryClient.prefetchQuery(key, async () => {

packages/query-core/src/types.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,12 @@ export interface QueryOptions<
7676
behavior?: QueryBehavior<TQueryFnData, TError, TData>
7777
/**
7878
* Set this to `false` to disable structural sharing between query results.
79+
* Set this to a function which accepts the old and new data and returns resolved data of the same type to implement custom structural sharing logic.
7980
* Defaults to `true`.
8081
*/
81-
structuralSharing?: boolean
82+
structuralSharing?:
83+
| boolean
84+
| ((oldData: TData | undefined, newData: TData) => TData)
8285
/**
8386
* This function can be set to automatically get the previous cursor for infinite queries.
8487
* The result will also be used to determine the value of `hasPreviousPage`.

packages/query-core/src/utils.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,8 @@ export function replaceData<
427427
// Use prev data if an isDataEqual function is defined and returns `true`
428428
if (options.isDataEqual?.(prevData, data)) {
429429
return prevData as TData
430+
} else if (typeof options.structuralSharing === 'function') {
431+
return options.structuralSharing(prevData, data)
430432
} else if (options.structuralSharing !== false) {
431433
// Structurally share data between prev and new data if needed
432434
return replaceEqualDeep(prevData, data)

0 commit comments

Comments
 (0)