Skip to content

Commit 10493ef

Browse files
authored
refactor(core): switch from Object.defineProperty to Proxies (#9079)
* ref: switch from Object.defineProperty to Proxies * test: improve the test that tracks property access
1 parent 8c7df63 commit 10493ef

File tree

3 files changed

+21
-31
lines changed

3 files changed

+21
-31
lines changed

docs/framework/react/guides/render-optimizations.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@ The top level object returned from `useQuery`, `useInfiniteQuery`, `useMutation`
1717

1818
## tracked properties
1919

20-
React Query will only trigger a re-render if one of the properties returned from `useQuery` is actually "used". This is done by using [custom getters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty#custom_setters_and_getters). This avoids a lot of unnecessary re-renders, e.g. because properties like `isFetching` or `isStale` might change often, but are not used in the component.
20+
React Query will only trigger a re-render if one of the properties returned from `useQuery` is actually "used". This is done by using [Proxy object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy). This avoids a lot of unnecessary re-renders, e.g. because properties like `isFetching` or `isStale` might change often, but are not used in the component.
2121

2222
You can customize this feature by setting `notifyOnChangeProps` manually globally or on a per-query basis. If you want to turn that feature off, you can set `notifyOnChangeProps: 'all'`.
2323

24-
> Note: Custom getters are invoked by accessing a property, either via destructuring or by accessing it directly. If you use object rest destructuring, you will disable this optimization. We have a [lint rule](../../../eslint/no-rest-destructuring.md) to guard against this pitfall.
24+
> Note: The get trap of a proxy is invoked by accessing a property, either via destructuring or by accessing it directly. If you use object rest destructuring, you will disable this optimization. We have a [lint rule](../../../eslint/no-rest-destructuring.md) to guard against this pitfall.
2525
2626
## select
2727

packages/query-core/src/queryObserver.ts

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -268,21 +268,13 @@ export class QueryObserver<
268268
result: QueryObserverResult<TData, TError>,
269269
onPropTracked?: (key: keyof QueryObserverResult) => void,
270270
): QueryObserverResult<TData, TError> {
271-
const trackedResult = {} as QueryObserverResult<TData, TError>
272-
273-
Object.keys(result).forEach((key) => {
274-
Object.defineProperty(trackedResult, key, {
275-
configurable: false,
276-
enumerable: true,
277-
get: () => {
278-
this.trackProp(key as keyof QueryObserverResult)
279-
onPropTracked?.(key as keyof QueryObserverResult)
280-
return result[key as keyof QueryObserverResult]
281-
},
282-
})
271+
return new Proxy(result, {
272+
get: (target, key) => {
273+
this.trackProp(key as keyof QueryObserverResult)
274+
onPropTracked?.(key as keyof QueryObserverResult)
275+
return Reflect.get(target, key)
276+
},
283277
})
284-
285-
return trackedResult
286278
}
287279

288280
trackProp(key: keyof QueryObserverResult) {

packages/react-query/src/__tests__/useQuery.test.tsx

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -905,42 +905,40 @@ describe('useQuery', () => {
905905
it('should track properties and only re-render when a tracked property changes', async () => {
906906
const key = queryKey()
907907
const states: Array<UseQueryResult<string>> = []
908+
let count = 0
908909

909910
function Page() {
910911
const state = useQuery({
911912
queryKey: key,
912913
queryFn: async () => {
913914
await sleep(10)
914-
return 'test'
915+
count++
916+
return 'test' + count
915917
},
916918
})
917919

918920
states.push(state)
919921

920-
const { refetch, data } = state
921-
922-
React.useEffect(() => {
923-
setActTimeout(() => {
924-
if (data) {
925-
refetch()
926-
}
927-
}, 20)
928-
}, [refetch, data])
929-
930922
return (
931923
<div>
932-
<h1>{data ?? null}</h1>
924+
<h1>{state.data ?? null}</h1>
925+
<button onClick={() => state.refetch()}>refetch</button>
933926
</div>
934927
)
935928
}
936929

937930
const rendered = renderWithClient(queryClient, <Page />)
938931

939-
await waitFor(() => rendered.getByText('test'))
932+
await waitFor(() => rendered.getByText('test1'))
940933

941-
expect(states.length).toBe(2)
934+
fireEvent.click(rendered.getByRole('button', { name: /refetch/i }))
935+
936+
await waitFor(() => rendered.getByText('test2'))
937+
938+
expect(states.length).toBe(3)
942939
expect(states[0]).toMatchObject({ data: undefined })
943-
expect(states[1]).toMatchObject({ data: 'test' })
940+
expect(states[1]).toMatchObject({ data: 'test1' })
941+
expect(states[2]).toMatchObject({ data: 'test2' })
944942
})
945943

946944
it('should always re-render if we are tracking props but not using any', async () => {

0 commit comments

Comments
 (0)