Repository: tannerlinsley/react-query
Pull request #966 (2020-09-05 08:08:59)

fix: make sure setQueryData is not considered as initial data

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/34m00n1p8](https://vercel.com/tannerlinsley/react-query/34m00n1p8)  
✅ Preview: *In Progress*



Patch:
diff --git a/docs/src/pages/docs/api.md b/docs/src/pages/docs/api.md
index 5bad26f35..81f3b1206 100644
--- a/docs/src/pages/docs/api.md
+++ b/docs/src/pages/docs/api.md
@@ -83,7 +83,7 @@ const queryInfo = useQuery({
   - A function like `attempt => Math.min(attempt > 1 ? 2 ** attempt * 1000 : 1000, 30 * 1000)` applies exponential backoff.
   - A function like `attempt => attempt * 1000` applies linear backoff.
 - `staleTime: Int | Infinity`
-  - The time in milliseconds that cache data remains fresh. After a successful cache update, that cache data will become stale after this duration.
+  - The time in milliseconds after data is considered stale.
   - If set to `Infinity`, query will never go stale
 - `cacheTime: Int | Infinity`
   - The time in milliseconds that unused/inactive cache data remains in memory. When a query's cache becomes unused or inactive, that cache data will be garbage collected after this duration.
diff --git a/src/core/query.ts b/src/core/query.ts
index 1790ba06d..14d076b4b 100644
--- a/src/core/query.ts
+++ b/src/core/query.ts
@@ -591,11 +591,11 @@ function getDefaultState<TResult, TError>(
   return {
     ...getStatusProps(initialStatus),
     error: null,
-    isFetched: false,
+    isFetched: Boolean(config.initialFetched),
     isFetching: initialStatus === QueryStatus.Loading,
     isFetchingMore: false,
     failureCount: 0,
-    fetchedCount: 0,
+    fetchedCount: config.initialFetched ? 1 : 0,
     data: initialData,
     updatedAt: Date.now(),
     canFetchMore: hasMorePages(config, initialData),
diff --git a/src/core/queryCache.ts b/src/core/queryCache.ts
index bc5a1cdbd..96dfe5d12 100644
--- a/src/core/queryCache.ts
+++ b/src/core/queryCache.ts
@@ -339,7 +339,7 @@ export class QueryCache {
     }
 
     this.buildQuery<TResult, TError>(queryKey, {
-      initialStale: typeof config?.staleTime === 'undefined',
+      initialFetched: true,
       initialData: functionalUpdate(updater, undefined),
       ...config,
     })
diff --git a/src/core/queryObserver.ts b/src/core/queryObserver.ts
index fc38b3160..74c88dcfa 100644
--- a/src/core/queryObserver.ts
+++ b/src/core/queryObserver.ts
@@ -171,7 +171,7 @@ export class QueryObserver<TResult, TError> {
     }
 
     const timeElapsed = Date.now() - updatedAt
-    const timeUntilStale = staleTime - timeElapsed
+    const timeUntilStale = staleTime - timeElapsed + 1
     const timeout = Math.max(timeUntilStale, 0)
 
     this.staleTimeoutId = setTimeout(() => {
@@ -244,7 +244,7 @@ export class QueryObserver<TResult, TError> {
       isPreviousData = true
     }
 
-    let isStale = false
+    let isStale
 
     // When the query has not been fetched yet and this is the initial render,
     // determine the staleness based on the initialStale or existence of initial data.
diff --git a/src/core/types.ts b/src/core/types.ts
index 9e363e8bb..06339cc88 100644
--- a/src/core/types.ts
+++ b/src/core/types.ts
@@ -42,7 +42,6 @@ export interface BaseQueryConfig<TResult, TError = unknown, TData = TResult> {
    */
   retry?: boolean | number | ((failureCount: number, error: TError) => boolean)
   retryDelay?: number | ((retryAttempt: number) => number)
-  staleTime?: number
   cacheTime?: number
   isDataEqual?: (oldData: unknown, newData: unknown) => boolean
   queryFn?: QueryFunction<TData>
@@ -50,7 +49,7 @@ export interface BaseQueryConfig<TResult, TError = unknown, TData = TResult> {
   queryKeySerializerFn?: QueryKeySerializerFunction
   queryFnParamsFilter?: (args: ArrayQueryKey) => ArrayQueryKey
   initialData?: TResult | InitialDataFunction<TResult>
-  initialStale?: boolean | InitialStaleFunction
+  initialFetched?: boolean
   infinite?: true
   /**
    * Set this to `false` to disable structural sharing between query results.
@@ -75,6 +74,17 @@ export interface QueryObserverConfig<
    * Defaults to `true`.
    */
   enabled?: boolean | unknown
+  /**
+   * The time in milliseconds after data is considered stale.
+   * If set to `Infinity`, the data will never be stale.
+   */
+  staleTime?: number
+  /**
+   * If set, this will mark any `initialData` provided as stale and will likely cause it to be refetched on mount.
+   * If a function is passed, it will be called only when appropriate to resolve the `initialStale` value.
+   * This can be useful if your `initialStale` value is costly to calculate.
+   */
+  initialStale?: boolean | InitialStaleFunction
   /**
    * If set to a number, the query will continuously refetch at this frequency in milliseconds.
    * Defaults to `false`.
diff --git a/src/hydration/hydration.ts b/src/hydration/hydration.ts
index 64b424230..8a40cd890 100644
--- a/src/hydration/hydration.ts
+++ b/src/hydration/hydration.ts
@@ -83,10 +83,8 @@ export function hydrate<TResult>(
 
   for (const dehydratedQuery of queries) {
     const queryKey = dehydratedQuery.config.queryKey
-    const queryConfig: QueryConfig<TResult> = dehydratedQuery.config as QueryConfig<
-      TResult
-    >
-
+    const queryConfig = dehydratedQuery.config as QueryConfig<TResult>
+    queryConfig.initialFetched = true
     const query = queryCache.buildQuery(queryKey, queryConfig)
     query.state.updatedAt = dehydratedQuery.updatedAt
   }


Test patch:
diff --git a/src/react/tests/useQuery.test.tsx b/src/react/tests/useQuery.test.tsx
index 7395ff7be..ff091c2fc 100644
--- a/src/react/tests/useQuery.test.tsx
+++ b/src/react/tests/useQuery.test.tsx
@@ -1225,6 +1225,41 @@ describe('useQuery', () => {
     consoleMock.mockRestore()
   })
 
+  it('should fetch on mount when a query was already created with setQueryData', async () => {
+    const key = queryKey()
+    const states: QueryResult<string>[] = []
+
+    queryCache.setQueryData(key, 'prefetched')
+
+    function Page() {
+      const state = useQuery(key, () => 'data')
+      states.push(state)
+      return null
+    }
+
+    render(<Page />)
+
+    await waitFor(() =>
+      expect(states).toMatchObject([
+        {
+          data: 'prefetched',
+          isFetching: false,
+          isStale: true,
+        },
+        {
+          data: 'prefetched',
+          isFetching: true,
+          isStale: true,
+        },
+        {
+          data: 'data',
+          isFetching: false,
+          isStale: true,
+        },
+      ])
+    )
+  })
+
   it('should refetch after focus regain', async () => {
     const key = queryKey()
     const states: QueryResult<string>[] = []
@@ -1245,7 +1280,7 @@ describe('useQuery', () => {
 
     render(<Page />)
 
-    await waitFor(() => expect(states.length).toBe(2))
+    await waitFor(() => expect(states.length).toBe(3))
 
     act(() => {
       // reset visibilityState to original value
@@ -1253,21 +1288,26 @@ describe('useQuery', () => {
       window.dispatchEvent(new FocusEvent('focus'))
     })
 
-    await waitFor(() => expect(states.length).toBe(4))
+    await waitFor(() => expect(states.length).toBe(5))
 
     expect(states).toMatchObject([
       {
         data: 'prefetched',
         isFetching: false,
-        isStale: false,
+        isStale: true,
       },
       {
         data: 'prefetched',
+        isFetching: true,
+        isStale: true,
+      },
+      {
+        data: 'data',
         isFetching: false,
         isStale: true,
       },
       {
-        data: 'prefetched',
+        data: 'data',
         isFetching: true,
         isStale: true,
       },