Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/framework/react/guides/render-optimizations.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
20 changes: 6 additions & 14 deletions packages/query-core/src/queryObserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,21 +268,13 @@
result: QueryObserverResult<TData, TError>,
onPropTracked?: (key: keyof QueryObserverResult) => void,
): QueryObserverResult<TData, TError> {
const trackedResult = {} as QueryObserverResult<TData, TError>

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)

Check warning on line 275 in packages/query-core/src/queryObserver.ts

View check run for this annotation

Codecov / codecov/patch

packages/query-core/src/queryObserver.ts#L272-L275

Added lines #L272 - L275 were not covered by tests
},
})

return trackedResult
}

trackProp(key: keyof QueryObserverResult) {
Expand Down
28 changes: 13 additions & 15 deletions packages/react-query/src/__tests__/useQuery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<UseQueryResult<string>> = []
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 (
<div>
<h1>{data ?? null}</h1>
<h1>{state.data ?? null}</h1>
<button onClick={() => state.refetch()}>refetch</button>
</div>
)
}

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

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 () => {
Expand Down