Render Optimizations

· abundance's blog


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:

structural sharing #

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.

referential identity #

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.

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. 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.

select #

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.

 1export const useTodos = (select) => {
 2  return useQuery({
 3    queryKey: ['todos'],
 4    queryFn: fetchTodos,
 5    select,
 6  })
 7}
 8
 9export const useTodoCount = () => {
10  return useTodos((data) => data.length)
11}

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.

memoization #

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:

1// wrapped in useCallback
2export const useTodoCount = () => {
3  return useTodos(useCallback((data) => data.length, []))
4}
1// extracted to a stable function reference
2const selectTodoCount = (data) => data.length
3
4export const useTodoCount = () => {
5  return useTodos(selectTodoCount)
6}

Further Reading #

For an in-depth guide about these topics, read React Query Render Optimizations from the Community Resources.