React Query applies a couple of optimizations automatically to ensure that your components only re-render when they actually need to. This is done by the following means:
React Query uses a technique called "structural sharing" to ensure that as many references as possible will be kept intact between re-renders. If data is fetched over the network, usually, you'll get a completely new reference by json parsing the response. However, React Query will keep the original reference if nothing changed in the data. If a subset changed, React Query will keep the unchanged parts and only replace the changed parts.
Note: This optimization only works if the queryFn returns JSON compatible data. You can turn it off by setting structuralSharing: false globally or on a per-query basis, or you can implement your own structural sharing by passing a function to it.
The top level object returned from useQuery, useInfiniteQuery, useMutation and the Array returned from useQueries is not referentially stable. It will be a new reference on every render. However, the data properties returned from these hooks will be as stable as possible.
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. 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 to guard against this pitfall.
You can use the select option to select a subset of the data that your component should subscribe to. This is useful for highly optimized data transformations or to avoid unnecessary re-renders.
export const useTodos = (select) => {
return useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
select,
})
}
export const useTodoCount = () => {
return useTodos((data) => data.length)
}
export const useTodos = (select) => {
return useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
select,
})
}
export const useTodoCount = () => {
return useTodos((data) => data.length)
}
A component using the useTodoCount custom hook will only re-render if the length of the todos changes. It will not re-render if e.g. the name of a todo changed.
Note: select operates on successfully cached data and is not the appropriate place to throw errors. The source of truth for errors is the queryFn, and a select function that returns an error results in data being undefined and isSuccess being true. We recommend handling errors in the queryFn if you wish to have a query fail on incorrect data, or outside of the query hook if you have a error case not related to caching.
The select function will only re-run if:
This means that an inlined select function, as shown above, will run on every render. To avoid this, you can wrap the select function in useCallback, or extract it to a stable function reference if it doesn't have any dependencies:
// wrapped in useCallback
export const useTodoCount = () => {
return useTodos(useCallback((data) => data.length, []))
}
// wrapped in useCallback
export const useTodoCount = () => {
return useTodos(useCallback((data) => data.length, []))
}
// extracted to a stable function reference
const selectTodoCount = (data) => data.length
export const useTodoCount = () => {
return useTodos(selectTodoCount)
}
// extracted to a stable function reference
const selectTodoCount = (data) => data.length
export const useTodoCount = () => {
return useTodos(selectTodoCount)
}
For an in-depth guide about these topics, read React Query Render Optimizations from the Community Resources.