diff --git a/docs/framework/react/guides/render-optimizations.md b/docs/framework/react/guides/render-optimizations.md index 51ca5b17d27..75775eab347 100644 --- a/docs/framework/react/guides/render-optimizations.md +++ b/docs/framework/react/guides/render-optimizations.md @@ -17,11 +17,11 @@ The top level object returned from `useQuery`, `useInfiniteQuery`, `useMutation` ## tracked properties -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. +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. 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'`. -> 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. +> 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. ## select diff --git a/packages/query-core/src/queryObserver.ts b/packages/query-core/src/queryObserver.ts index 6f01860d304..174dc72bf50 100644 --- a/packages/query-core/src/queryObserver.ts +++ b/packages/query-core/src/queryObserver.ts @@ -268,21 +268,13 @@ export class QueryObserver< result: QueryObserverResult, onPropTracked?: (key: keyof QueryObserverResult) => void, ): QueryObserverResult { - const trackedResult = {} as QueryObserverResult - - Object.keys(result).forEach((key) => { - Object.defineProperty(trackedResult, key, { - configurable: false, - enumerable: true, - get: () => { - this.trackProp(key as keyof QueryObserverResult) - onPropTracked?.(key as keyof QueryObserverResult) - return result[key as keyof QueryObserverResult] - }, - }) + return new Proxy(result, { + get: (target, key) => { + this.trackProp(key as keyof QueryObserverResult) + onPropTracked?.(key as keyof QueryObserverResult) + return Reflect.get(target, key) + }, }) - - return trackedResult } trackProp(key: keyof QueryObserverResult) { diff --git a/packages/react-query/src/__tests__/useQuery.test.tsx b/packages/react-query/src/__tests__/useQuery.test.tsx index 935b0734c1a..dfea088fd04 100644 --- a/packages/react-query/src/__tests__/useQuery.test.tsx +++ b/packages/react-query/src/__tests__/useQuery.test.tsx @@ -905,42 +905,40 @@ describe('useQuery', () => { it('should track properties and only re-render when a tracked property changes', async () => { const key = queryKey() const states: Array> = [] + let count = 0 function Page() { const state = useQuery({ queryKey: key, queryFn: async () => { await sleep(10) - return 'test' + count++ + return 'test' + count }, }) states.push(state) - const { refetch, data } = state - - React.useEffect(() => { - setActTimeout(() => { - if (data) { - refetch() - } - }, 20) - }, [refetch, data]) - return (
-

{data ?? null}

+

{state.data ?? null}

+
) } const rendered = renderWithClient(queryClient, ) - await waitFor(() => rendered.getByText('test')) + await waitFor(() => rendered.getByText('test1')) - expect(states.length).toBe(2) + fireEvent.click(rendered.getByRole('button', { name: /refetch/i })) + + await waitFor(() => rendered.getByText('test2')) + + expect(states.length).toBe(3) expect(states[0]).toMatchObject({ data: undefined }) - expect(states[1]).toMatchObject({ data: 'test' }) + expect(states[1]).toMatchObject({ data: 'test1' }) + expect(states[2]).toMatchObject({ data: 'test2' }) }) it('should always re-render if we are tracking props but not using any', async () => {