Repository: tannerlinsley/react-query
Pull request #1487 (2020-12-19 09:02:25)

fix: prevent redundant renders
With the change in https://github.com/tannerlinsley/react-query/pull/1449 components will always render the latest result instead of waiting for an update from the `QueryObserver`. This means that in some cases, the update is redundant because it has already been rendered. This PR will prevent those redundant renders.

Might fix: https://github.com/tannerlinsley/react-query/issues/1482

vercel[bot]: This pull request is being automatically deployed with Vercel ([learn more](https://vercel.link/github-learn-more)).  
To see the status of your deployment, click below or on the icon next to each commit.

🔍 Inspect: [https://vercel.com/tannerlinsley/react-query/pws45bhu5](https://vercel.com/tannerlinsley/react-query/pws45bhu5)  
✅ Preview: [https://react-query-git-feature-prevent-redundant-renders.tannerlinsley.vercel.app](https://react-query-git-feature-prevent-redundant-renders.tannerlinsley.vercel.app)



Patch:
diff --git a/src/core/queryObserver.ts b/src/core/queryObserver.ts
index c3ae314c6..a9e998ff6 100644
--- a/src/core/queryObserver.ts
+++ b/src/core/queryObserver.ts
@@ -409,7 +409,7 @@ export class QueryObserver<
       data,
       dataUpdatedAt,
       error: state.error,
-      errorUpdatedAt: state.errorUpdateCount,
+      errorUpdatedAt: state.errorUpdatedAt,
       failureCount: state.fetchFailureCount,
       isFetched: state.dataUpdateCount > 0 || state.errorUpdateCount > 0,
       isFetchedAfterMount:
diff --git a/src/react/useBaseQuery.ts b/src/react/useBaseQuery.ts
index be4a30121..3aa1b53c0 100644
--- a/src/react/useBaseQuery.ts
+++ b/src/react/useBaseQuery.ts
@@ -2,6 +2,7 @@ import React from 'react'
 
 import { notifyManager } from '../core/notifyManager'
 import { QueryObserver } from '../core/queryObserver'
+import { QueryObserverResult } from '../core/types'
 import { useQueryErrorResetBoundary } from './QueryErrorResetBoundary'
 import { useQueryClient } from './QueryClientProvider'
 import { UseBaseQueryOptions } from './types'
@@ -56,10 +57,20 @@ export function useBaseQuery<TQueryFnData, TError, TData, TQueryData>(
   const [, rerender] = React.useState({})
   const currentResult = observer.getCurrentResult()
 
+  // Remember latest result to prevent redundant renders
+  const latestResultRef = React.useRef(currentResult)
+  latestResultRef.current = currentResult
+
   // Subscribe to the observer
   React.useEffect(() => {
     errorResetBoundary.clearReset()
-    return observer.subscribe(notifyManager.batchCalls(rerender))
+    return observer.subscribe(
+      notifyManager.batchCalls((result: QueryObserverResult) => {
+        if (result !== latestResultRef.current) {
+          rerender({})
+        }
+      })
+    )
   }, [observer, errorResetBoundary])
 
   // Handle suspense


Test patch:
diff --git a/src/react/tests/suspense.test.tsx b/src/react/tests/suspense.test.tsx
index d2c769212..a8534bc88 100644
--- a/src/react/tests/suspense.test.tsx
+++ b/src/react/tests/suspense.test.tsx
@@ -9,12 +9,62 @@ import {
   QueryCache,
   QueryErrorResetBoundary,
   useQueryErrorResetBoundary,
+  UseQueryResult,
 } from '../..'
 
 describe("useQuery's in Suspense mode", () => {
   const queryCache = new QueryCache()
   const queryClient = new QueryClient({ queryCache })
 
+  it('should render the correct amount of times in Suspense mode', async () => {
+    const key = queryKey()
+    const states: UseQueryResult<number>[] = []
+
+    let count = 0
+    let renders = 0
+
+    function Page() {
+      renders++
+
+      const [stateKey, setStateKey] = React.useState(key)
+
+      const state = useQuery(
+        stateKey,
+        async () => {
+          count++
+          await sleep(10)
+          return count
+        },
+        { suspense: true }
+      )
+
+      states.push(state)
+
+      return (
+        <button aria-label="toggle" onClick={() => setStateKey(queryKey())} />
+      )
+    }
+
+    const rendered = renderWithClient(
+      queryClient,
+      <React.Suspense fallback="loading">
+        <Page />
+      </React.Suspense>
+    )
+
+    await sleep(20)
+
+    await waitFor(() => rendered.getByLabelText('toggle'))
+    fireEvent.click(rendered.getByLabelText('toggle'))
+
+    await sleep(20)
+
+    expect(renders).toBe(4)
+    expect(states.length).toBe(2)
+    expect(states[0]).toMatchObject({ data: 1, status: 'success' })
+    expect(states[1]).toMatchObject({ data: 2, status: 'success' })
+  })
+
   it('should not call the queryFn twice when used in Suspense mode', async () => {
     const key = queryKey()
 
diff --git a/src/react/tests/useInfiniteQuery.test.tsx b/src/react/tests/useInfiniteQuery.test.tsx
index 86cdee785..6a0e1e894 100644
--- a/src/react/tests/useInfiniteQuery.test.tsx
+++ b/src/react/tests/useInfiniteQuery.test.tsx
@@ -206,7 +206,7 @@ describe('useInfiniteQuery', () => {
 
     await sleep(300)
 
-    expect(states.length).toBe(7)
+    expect(states.length).toBe(6)
     expect(states[0]).toMatchObject({
       data: undefined,
       isFetching: true,
@@ -244,13 +244,6 @@ describe('useInfiniteQuery', () => {
       isPreviousData: true,
     })
     expect(states[5]).toMatchObject({
-      data: { pages: ['0-desc', '1-desc'] },
-      isFetching: true,
-      isFetchingNextPage: false,
-      isSuccess: true,
-      isPreviousData: true,
-    })
-    expect(states[6]).toMatchObject({
       data: { pages: ['0-asc'] },
       isFetching: false,
       isFetchingNextPage: false,
@@ -823,7 +816,7 @@ describe('useInfiniteQuery', () => {
 
     await sleep(100)
 
-    expect(states.length).toBe(6)
+    expect(states.length).toBe(5)
     expect(states[0]).toMatchObject({
       hasNextPage: undefined,
       data: undefined,
@@ -847,16 +840,8 @@ describe('useInfiniteQuery', () => {
       isFetchingNextPage: false,
       isSuccess: true,
     })
-    // Cache update
-    expect(states[3]).toMatchObject({
-      hasNextPage: true,
-      data: { pages: [7, 8] },
-      isFetching: false,
-      isFetchingNextPage: false,
-      isSuccess: true,
-    })
     // Refetch
-    expect(states[4]).toMatchObject({
+    expect(states[3]).toMatchObject({
       hasNextPage: true,
       data: { pages: [7, 8] },
       isFetching: true,
@@ -864,7 +849,7 @@ describe('useInfiniteQuery', () => {
       isSuccess: true,
     })
     // Refetch done
-    expect(states[5]).toMatchObject({
+    expect(states[4]).toMatchObject({
       hasNextPage: true,
       data: { pages: [7, 8] },
       isFetching: false,
diff --git a/src/react/tests/useQuery.test.tsx b/src/react/tests/useQuery.test.tsx
index f8951b7cb..098526523 100644
--- a/src/react/tests/useQuery.test.tsx
+++ b/src/react/tests/useQuery.test.tsx
@@ -568,17 +568,15 @@ describe('useQuery', () => {
 
     await sleep(100)
 
-    expect(states.length).toBe(5)
+    expect(states.length).toBe(4)
     // First load
     expect(states[0]).toMatchObject({ isLoading: true, isSuccess: false })
     // First success
     expect(states[1]).toMatchObject({ isLoading: false, isSuccess: true })
-    // Switch
-    expect(states[2]).toMatchObject({ isLoading: true, isSuccess: false })
     // Second load
-    expect(states[3]).toMatchObject({ isLoading: true, isSuccess: false })
+    expect(states[2]).toMatchObject({ isLoading: true, isSuccess: false })
     // Second success
-    expect(states[4]).toMatchObject({ isLoading: false, isSuccess: true })
+    expect(states[3]).toMatchObject({ isLoading: false, isSuccess: true })
   })
 
   it('should fetch when refetchOnMount is false and nothing has been fetched yet', async () => {
@@ -768,17 +766,15 @@ describe('useQuery', () => {
 
     await sleep(20)
 
-    expect(states.length).toBe(5)
+    expect(states.length).toBe(4)
     // Initial
     expect(states[0]).toMatchObject({ data: undefined })
     // Fetched
     expect(states[1]).toMatchObject({ data: 1 })
-    // Rerender
-    expect(states[2]).toMatchObject({ data: undefined })
     // Switch
-    expect(states[3]).toMatchObject({ data: undefined })
+    expect(states[2]).toMatchObject({ data: undefined })
     // Fetched
-    expect(states[4]).toMatchObject({ data: 2 })
+    expect(states[3]).toMatchObject({ data: 2 })
   })
 
   it('should be create a new query when refetching a removed query', async () => {
@@ -1084,7 +1080,7 @@ describe('useQuery', () => {
 
     renderWithClient(queryClient, <Page />)
 
-    await waitFor(() => expect(states.length).toBe(5))
+    await waitFor(() => expect(states.length).toBe(4))
 
     // Initial
     expect(states[0]).toMatchObject({
@@ -1107,15 +1103,8 @@ describe('useQuery', () => {
       isSuccess: true,
       isPreviousData: true,
     })
-    // Previous data
-    expect(states[3]).toMatchObject({
-      data: 0,
-      isFetching: true,
-      isSuccess: true,
-      isPreviousData: true,
-    })
     // New data
-    expect(states[4]).toMatchObject({
+    expect(states[3]).toMatchObject({
       data: 1,
       isFetching: false,
       isSuccess: true,
@@ -1152,7 +1141,7 @@ describe('useQuery', () => {
 
     renderWithClient(queryClient, <Page />)
 
-    await waitFor(() => expect(states.length).toBe(5))
+    await waitFor(() => expect(states.length).toBe(4))
 
     // Initial
     expect(states[0]).toMatchObject({
@@ -1175,15 +1164,8 @@ describe('useQuery', () => {
       isSuccess: true,
       isPreviousData: true,
     })
-    // Previous data
-    expect(states[3]).toMatchObject({
-      data: 0,
-      isFetching: true,
-      isSuccess: true,
-      isPreviousData: true,
-    })
     // New data
-    expect(states[4]).toMatchObject({
+    expect(states[3]).toMatchObject({
       data: 1,
       isFetching: false,
       isSuccess: true,
@@ -1228,7 +1210,7 @@ describe('useQuery', () => {
 
     renderWithClient(queryClient, <Page />)
 
-    await waitFor(() => expect(states.length).toBe(7))
+    await waitFor(() => expect(states.length).toBe(6))
 
     // Disabled query
     expect(states[0]).toMatchObject({
@@ -1258,22 +1240,15 @@ describe('useQuery', () => {
       isSuccess: true,
       isPreviousData: true,
     })
-    // Switched query key
-    expect(states[4]).toMatchObject({
-      data: 0,
-      isFetching: false,
-      isSuccess: true,
-      isPreviousData: true,
-    })
     // Fetching new query
-    expect(states[5]).toMatchObject({
+    expect(states[4]).toMatchObject({
       data: 0,
       isFetching: true,
       isSuccess: true,
       isPreviousData: true,
     })
     // Fetched new query
-    expect(states[6]).toMatchObject({
+    expect(states[5]).toMatchObject({
       data: 1,
       isFetching: false,
       isSuccess: true,
@@ -1324,7 +1299,7 @@ describe('useQuery', () => {
 
     await sleep(100)
 
-    expect(states.length).toBe(6)
+    expect(states.length).toBe(5)
 
     // Disabled query
     expect(states[0]).toMatchObject({
@@ -1340,29 +1315,22 @@ describe('useQuery', () => {
       isSuccess: true,
       isPreviousData: true,
     })
-    // Switched query key
-    expect(states[2]).toMatchObject({
-      data: 10,
-      isFetching: false,
-      isSuccess: true,
-      isPreviousData: true,
-    })
     // Set state
-    expect(states[3]).toMatchObject({
+    expect(states[2]).toMatchObject({
       data: 10,
       isFetching: false,
       isSuccess: true,
       isPreviousData: true,
     })
     // Switched query key
-    expect(states[4]).toMatchObject({
+    expect(states[3]).toMatchObject({
       data: 10,
       isFetching: true,
       isSuccess: true,
       isPreviousData: true,
     })
     // Refetch done
-    expect(states[5]).toMatchObject({
+    expect(states[4]).toMatchObject({
       data: 12,
       isFetching: false,
       isSuccess: true,
@@ -2126,13 +2094,11 @@ describe('useQuery', () => {
 
     await sleep(100)
 
-    expect(states.length).toBe(3)
+    expect(states.length).toBe(2)
     // Initial
     expect(states[0]).toMatchObject({ data: { count: 0 } })
     // Set state
     expect(states[1]).toMatchObject({ data: { count: 1 } })
-    // Update
-    expect(states[2]).toMatchObject({ data: { count: 1 } })
   })
 
   it('should retry specified number of times', async () => {