Repository: tannerlinsley/react-query (ts)

Issue:
SSR prefetching (hydration/dehydration) with NextJS and client side routing to the same route.
**Describe the bug**

Not sure if this is a bug or if I'm doing something incorrectly but when using SSR hydration/dehydration prefetching based on the [docs](https://react-query.tanstack.com/docs/guides/ssr#integrating-with-nextjs), the first request will set initial data for a query correctly, but when routing to the same route but with a different param the cache isn't updated with the new prefetched value.

**To Reproduce**
Steps to reproduce the behavior:
1. Go to https://ctwew.sse.codesandbox.io/set1
2. Open up the dev tools and see that the data returned from getServerSideProps and the data displayed is the same.
3. Click on toggle set, this will perform a client side route change and will call getServeSideProps as an api call.
4. You will notice that the data onscreen will still have "dataSet: set1", the url will be "/set2". However if you check the console you will see that the data contains "dataSet: set2"

**Expected behavior**
I would expect the data from getServerSideProps to update the cache and the components using the data to be rerendered with the correct values.

**Screenshots**
https://codesandbox.io/s/amazing-paper-ctwew?file=/pages/%5BdataSet%5D.js

Data returned from getServerSideProps on the initial page load.
![Screenshot 2020-09-02 at 11 13 28](https://user-images.githubusercontent.com/33291351/91969007-669acc00-ed0d-11ea-95e9-41f45860caf7.png)

Data returned from getServerSideProps on the route change.
![Screenshot 2020-09-02 at 11 13 42](https://user-images.githubusercontent.com/33291351/91969074-7c0ff600-ed0d-11ea-9feb-e2a92cfc3e3b.png)


**Desktop (please complete the following information):**
 - OS: macOs
 - Browser: chrome
 - Version: 2.13.1


**Additional context**
I did notice the force option for the prefetch query options and have tried it with "force: true" but that didn't work either.


Pull request description:
fix(hydration): overwrite the existing data in the cache if hydrated data is newer
Fixes #951 

This makes it so hydrating queries that have newer data according to `updatedAt` will set this new data to the existing query in the cache (with the correct `updatedAt`).

This PR also moves some internal stuff around in hydration as well as changes the shape of the hydration payload. Hydrated queries are no longer set with `initialData`, instead `query.setData` with the new internal `updatedAt`-option is used.

Patch:
diff --git a/src/core/query.ts b/src/core/query.ts
index 4007fd003..bf59c3593 100644
--- a/src/core/query.ts
+++ b/src/core/query.ts
@@ -65,6 +65,10 @@ const enum ActionType {
   Error,
 }
 
+interface SetDataOptions {
+  updatedAt?: number
+}
+
 interface FailedAction {
   type: ActionType.Failed
 }
@@ -78,6 +82,7 @@ interface SuccessAction<TResult> {
   type: ActionType.Success
   data: TResult | undefined
   canFetchMore?: boolean
+  updatedAt?: number
 }
 
 interface ErrorAction<TError> {
@@ -175,7 +180,10 @@ export class Query<TResult, TError> {
     }
   }
 
-  setData(updater: Updater<TResult | undefined, TResult>): void {
+  setData(
+    updater: Updater<TResult | undefined, TResult>,
+    options?: SetDataOptions
+  ): void {
     const prevData = this.state.data
 
     // Get the new data
@@ -199,6 +207,7 @@ export class Query<TResult, TError> {
       type: ActionType.Success,
       data,
       canFetchMore,
+      updatedAt: options?.updatedAt,
     })
   }
 
@@ -630,7 +639,7 @@ export function queryReducer<TResult, TError>(
         isFetching: false,
         isFetchingMore: false,
         canFetchMore: action.canFetchMore,
-        updatedAt: Date.now(),
+        updatedAt: action.updatedAt ?? Date.now(),
         failureCount: 0,
       }
     case ActionType.Error:
diff --git a/src/hydration/hydration.ts b/src/hydration/hydration.ts
index 8a40cd890..d8c709213 100644
--- a/src/hydration/hydration.ts
+++ b/src/hydration/hydration.ts
@@ -3,14 +3,14 @@ import { DEFAULT_CACHE_TIME } from '../core/config'
 import type { Query, QueryCache, QueryKey, QueryConfig } from 'react-query'
 
 export interface DehydratedQueryConfig {
-  queryKey: QueryKey
   cacheTime?: number
-  initialData?: unknown
 }
 
 export interface DehydratedQuery {
-  config: DehydratedQueryConfig
+  queryKey: QueryKey
+  data?: unknown
   updatedAt: number
+  config: DehydratedQueryConfig
 }
 
 export interface DehydratedState {
@@ -28,9 +28,8 @@ function dehydrateQuery<TResult, TError = unknown>(
   query: Query<TResult, TError>
 ): DehydratedQuery {
   const dehydratedQuery: DehydratedQuery = {
-    config: {
-      queryKey: query.queryKey,
-    },
+    config: {},
+    queryKey: query.queryKey,
     updatedAt: query.state.updatedAt,
   }
 
@@ -44,7 +43,7 @@ function dehydrateQuery<TResult, TError = unknown>(
     dehydratedQuery.config.cacheTime = query.cacheTime
   }
   if (query.state.data !== undefined) {
-    dehydratedQuery.config.initialData = query.state.data
+    dehydratedQuery.data = query.state.data
   }
 
   return dehydratedQuery
@@ -82,10 +81,22 @@ export function hydrate<TResult>(
   const queries = (dehydratedState as DehydratedState).queries || []
 
   for (const dehydratedQuery of queries) {
-    const queryKey = dehydratedQuery.config.queryKey
+    const queryKey = dehydratedQuery.queryKey
     const queryConfig = dehydratedQuery.config as QueryConfig<TResult>
-    queryConfig.initialFetched = true
-    const query = queryCache.buildQuery(queryKey, queryConfig)
-    query.state.updatedAt = dehydratedQuery.updatedAt
+
+    let query = queryCache.getQuery<TResult>(queryKey)
+
+    if (query) {
+      if (query.state.updatedAt < dehydratedQuery.updatedAt) {
+        query.setData(dehydratedQuery.data as TResult, {
+          updatedAt: dehydratedQuery.updatedAt,
+        })
+      }
+    } else {
+      query = queryCache.buildQuery<TResult>(queryKey, queryConfig)
+      query.setData(dehydratedQuery.data as TResult, {
+        updatedAt: dehydratedQuery.updatedAt,
+      })
+    }
   }
 }


Test patch:
diff --git a/src/hydration/tests/hydration.test.tsx b/src/hydration/tests/hydration.test.tsx
index 1dcb83eb4..2ba9d4963 100644
--- a/src/hydration/tests/hydration.test.tsx
+++ b/src/hydration/tests/hydration.test.tsx
@@ -43,24 +43,24 @@ describe('dehydration and rehydration', () => {
 
     const fetchDataAfterHydration = jest.fn()
     await hydrationQueryCache.prefetchQuery('string', fetchDataAfterHydration, {
-      staleTime: 100,
+      staleTime: 1000,
     })
     await hydrationQueryCache.prefetchQuery('number', fetchDataAfterHydration, {
-      staleTime: 100,
+      staleTime: 1000,
     })
     await hydrationQueryCache.prefetchQuery(
       'boolean',
       fetchDataAfterHydration,
-      { staleTime: 100 }
+      { staleTime: 1000 }
     )
     await hydrationQueryCache.prefetchQuery('null', fetchDataAfterHydration, {
-      staleTime: 100,
+      staleTime: 1000,
     })
     await hydrationQueryCache.prefetchQuery('array', fetchDataAfterHydration, {
-      staleTime: 100,
+      staleTime: 1000,
     })
     await hydrationQueryCache.prefetchQuery('nested', fetchDataAfterHydration, {
-      staleTime: 100,
+      staleTime: 1000,
     })
     expect(fetchDataAfterHydration).toHaveBeenCalledTimes(0)
 
@@ -134,7 +134,7 @@ describe('dehydration and rehydration', () => {
     // Exact shape is not important here, just that staleTime and cacheTime
     // (and any future other config) is not included in it
     const dehydratedQuery = dehydrated?.queries.find(
-      query => (query?.config?.queryKey as Array<string>)[0] === 'string'
+      query => (query?.queryKey as Array<string>)[0] === 'string'
     )
     expect(dehydratedQuery).toBeTruthy()
     expect(dehydratedQuery?.config.cacheTime).toBe(undefined)
@@ -179,7 +179,7 @@ describe('dehydration and rehydration', () => {
     // This is testing implementation details that can change and are not
     // part of the public API, but is important for keeping the payload small
     const dehydratedQuery = dehydrated?.queries.find(
-      query => (query?.config?.queryKey as Array<string>)[0] === 'string'
+      query => (query?.queryKey as Array<string>)[0] === 'string'
     )
     expect(dehydratedQuery).toBeUndefined()
 
@@ -196,4 +196,52 @@ describe('dehydration and rehydration', () => {
     queryCache.clear({ notify: false })
     hydrationQueryCache.clear({ notify: false })
   })
+
+  test('should not overwrite query in cache if hydrated query is older', async () => {
+    const queryCache = makeQueryCache()
+    await queryCache.prefetchQuery('string', () => fetchData('string-older', 5))
+    const dehydrated = dehydrate(queryCache)
+    const stringified = JSON.stringify(dehydrated)
+
+    // ---
+
+    const parsed = JSON.parse(stringified)
+    const hydrationQueryCache = makeQueryCache()
+    await hydrationQueryCache.prefetchQuery('string', () =>
+      fetchData('string-newer', 5)
+    )
+
+    hydrate(hydrationQueryCache, parsed)
+    expect(hydrationQueryCache.getQuery('string')?.state.data).toBe(
+      'string-newer'
+    )
+
+    queryCache.clear({ notify: false })
+    hydrationQueryCache.clear({ notify: false })
+  })
+
+  test('should overwrite query in cache if hydrated query is newer', async () => {
+    const hydrationQueryCache = makeQueryCache()
+    await hydrationQueryCache.prefetchQuery('string', () =>
+      fetchData('string-older', 5)
+    )
+
+    // ---
+
+    const queryCache = makeQueryCache()
+    await queryCache.prefetchQuery('string', () => fetchData('string-newer', 5))
+    const dehydrated = dehydrate(queryCache)
+    const stringified = JSON.stringify(dehydrated)
+
+    // ---
+
+    const parsed = JSON.parse(stringified)
+    hydrate(hydrationQueryCache, parsed)
+    expect(hydrationQueryCache.getQuery('string')?.state.data).toBe(
+      'string-newer'
+    )
+
+    queryCache.clear({ notify: false })
+    hydrationQueryCache.clear({ notify: false })
+  })
 })
diff --git a/src/hydration/tests/react.test.tsx b/src/hydration/tests/react.test.tsx
index ba2a319de..9d50d4e0d 100644
--- a/src/hydration/tests/react.test.tsx
+++ b/src/hydration/tests/react.test.tsx
@@ -92,7 +92,7 @@ describe('React hydration', () => {
 
       const intermediateCache = makeQueryCache()
       await intermediateCache.prefetchQuery('string', () =>
-        dataQuery('should not change')
+        dataQuery('should change')
       )
       await intermediateCache.prefetchQuery('added string', dataQuery)
       const dehydrated = dehydrate(intermediateCache)
@@ -107,11 +107,11 @@ describe('React hydration', () => {
         </ReactQueryCacheProvider>
       )
 
-      // Existing query data should not be overwritten,
-      // so this should still be the original data
+      // Existing query data should be overwritten if older,
+      // so this should have changed
       await waitForMs(10)
-      rendered.getByText('string')
-      // But new query data should be available immediately
+      rendered.getByText('should change')
+      // New query data should be available immediately
       rendered.getByText('added string')
 
       clientQueryCache.clear({ notify: false })