diff --git a/docs/src/pages/docs/api.md b/docs/src/pages/docs/api.md
index 8e6353187a..b19bd2612e 100644
--- a/docs/src/pages/docs/api.md
+++ b/docs/src/pages/docs/api.md
@@ -7,40 +7,41 @@ title: API Reference
```js
const {
- status,
- isIdle,
- isLoading,
- isSuccess,
- isError,
+ clear,
data,
error,
- isStale,
- isFetching,
failureCount,
+ isError,
+ isFetching,
+ isIdle,
+ isLoading,
+ isStale,
+ isSuccess,
refetch,
- clear,
+ status,
} = useQuery(queryKey, queryFn?, {
- suspense,
- queryKeySerializerFn,
- enabled,
- retry,
- retryDelay,
- staleTime,
cacheTime,
+ enabled,
+ initialData,
+ initialStale,
+ isDataEqual,
keepPreviousData,
- refetchOnWindowFocus,
- refetchOnReconnect,
+ notifyOnStatusChange,
+ onError,
+ onSettled,
+ onSuccess,
+ queryFnParamsFilter,
+ queryKeySerializerFn,
refetchInterval,
refetchIntervalInBackground,
- queryFnParamsFilter,
refetchOnMount,
+ refetchOnReconnect,
+ refetchOnWindowFocus,
+ retry,
+ retryDelay,
+ staleTime,
structuralSharing,
- isDataEqual,
- onError,
- onSuccess,
- onSettled,
- initialData,
- initialStale,
+ suspense,
useErrorBoundary,
})
@@ -96,6 +97,11 @@ const queryInfo = useQuery({
- `refetchOnReconnect: Boolean`
- Optional
- Set this to `true` or `false` to enable/disable automatic refetching on reconnect for this query.
+- `notifyOnStatusChange: Boolean`
+ - Optional
+ - Whether a change to the query status should re-render a component.
+ - If set to `false`, the component will only re-render when the actual `data` or `error` changes.
+ - Defaults to `true`.
- `onSuccess: Function(data) => data`
- Optional
- This function will fire any time the query successfully fetches new data.
diff --git a/docs/src/pages/docs/comparison.md b/docs/src/pages/docs/comparison.md
index 68c7985a0a..53619eee32 100644
--- a/docs/src/pages/docs/comparison.md
+++ b/docs/src/pages/docs/comparison.md
@@ -19,7 +19,6 @@ Feature/Capability Key:
| Supported Query Keys | JSON | JSON | GraphQL Query |
| Query Key Change Detection | Deep Compare (Serialization) | Referential Equality (===) | Deep Compare (Serialization) |
| Query Data Memoization Level | Query + Structural Sharing | Query | Query + Entity + Structural Sharing |
-| Stale While Revalidate | Server-Side + Client-Side | Server-Side | None |
| Bundle Size | [![][bp-react-query]][bpl-react-query] | [![][bp-swr]][bpl-swr] | [![][bp-apollo]][bpl-apollo] |
| Queries | ✅ | ✅ | ✅ |
| Caching | ✅ | ✅ | ✅ |
@@ -38,6 +37,8 @@ Feature/Capability Key:
| Prefetching APIs | ✅ | 🔶 | ✅ |
| Query Cancellation | ✅ | 🛑 | 🛑 |
| Partial Query Matching2 | ✅ | 🛑 | 🛑 |
+| Stale While Revalidate | ✅ | ✅ | 🛑 |
+| Stale Time Configuration | ✅ | 🛑 | 🛑 |
| Window Focus Refetching | ✅ | ✅ | 🛑 |
| Network Status Refetching | ✅ | ✅ | ✅ |
| Automatic Refetch after Mutation3 | 🔶 | 🔶 | ✅ |
diff --git a/src/core/config.ts b/src/core/config.ts
index 55b639c208..801a2cb264 100644
--- a/src/core/config.ts
+++ b/src/core/config.ts
@@ -60,6 +60,7 @@ export const DEFAULT_CONFIG: ReactQueryConfig = {
refetchOnWindowFocus: true,
refetchOnReconnect: true,
refetchOnMount: true,
+ notifyOnStatusChange: true,
structuralSharing: true,
},
}
diff --git a/src/core/query.ts b/src/core/query.ts
index eaad23a613..2615123942 100644
--- a/src/core/query.ts
+++ b/src/core/query.ts
@@ -409,7 +409,7 @@ export class Query {
const config = this.config
// Check if there is a query function
- if (!config.queryFn) {
+ if (typeof config.queryFn !== 'function') {
return
}
diff --git a/src/core/queryCache.ts b/src/core/queryCache.ts
index 3c8c63dde1..74eaf11f46 100644
--- a/src/core/queryCache.ts
+++ b/src/core/queryCache.ts
@@ -313,7 +313,6 @@ export class QueryCache {
if (options?.throwOnError) {
throw error
}
- return
}
}
diff --git a/src/core/queryObserver.ts b/src/core/queryObserver.ts
index 505308ed0c..eb6c3c699f 100644
--- a/src/core/queryObserver.ts
+++ b/src/core/queryObserver.ts
@@ -217,22 +217,16 @@ export class QueryObserver {
}
private createResult(): QueryResult {
- const { currentResult, currentQuery, previousResult, config } = this
-
- const {
- canFetchMore,
- error,
- failureCount,
- isFetched,
- isFetching,
- isFetchingMore,
- isLoading,
- } = currentQuery.state
-
- let { data, status, updatedAt } = currentQuery.state
+ const { currentQuery, currentResult, previousResult, config } = this
+ const { state } = currentQuery
+ let { data, status, updatedAt } = state
// Keep previous data if needed
- if (config.keepPreviousData && isLoading && previousResult?.isSuccess) {
+ if (
+ config.keepPreviousData &&
+ state.isLoading &&
+ previousResult?.isSuccess
+ ) {
data = previousResult.data
updatedAt = previousResult.updatedAt
status = previousResult.status
@@ -256,15 +250,15 @@ export class QueryObserver {
return {
...getStatusProps(status),
- canFetchMore,
+ canFetchMore: state.canFetchMore,
clear: this.clear,
data,
- error,
- failureCount,
+ error: state.error,
+ failureCount: state.failureCount,
fetchMore: this.fetchMore,
- isFetched,
- isFetching,
- isFetchingMore,
+ isFetched: state.isFetched,
+ isFetching: state.isFetching,
+ isFetchingMore: state.isFetchingMore,
isStale,
query: currentQuery,
refetch: this.refetch,
@@ -303,20 +297,35 @@ export class QueryObserver {
_state: QueryState,
action: Action
): void {
- this.currentResult = this.createResult()
+ const { config } = this
- const { data, error, isSuccess, isError } = this.currentResult
+ // Store current result and get new result
+ const prevResult = this.currentResult
+ this.currentResult = this.createResult()
+ const result = this.currentResult
- if (action.type === 'Success' && isSuccess) {
- this.config.onSuccess?.(data!)
- this.config.onSettled?.(data!, null)
+ // We need to check the action because the state could have
+ // transitioned from success to success in case of `setQueryData`.
+ if (action.type === 'Success' && result.isSuccess) {
+ config.onSuccess?.(result.data!)
+ config.onSettled?.(result.data!, null)
this.updateTimers()
- } else if (action.type === 'Error' && isError) {
- this.config.onError?.(error!)
- this.config.onSettled?.(undefined, error!)
+ } else if (action.type === 'Error' && result.isError) {
+ config.onError?.(result.error!)
+ config.onSettled?.(undefined, result.error!)
this.updateTimers()
}
- this.updateListener?.(this.currentResult)
+ // Decide if we need to notify the listener
+ const notify =
+ // Always notify on data or error change
+ result.data !== prevResult.data ||
+ result.error !== prevResult.error ||
+ // Maybe notify on other changes
+ config.notifyOnStatusChange
+
+ if (notify) {
+ this.updateListener?.(result)
+ }
}
}
diff --git a/src/core/types.ts b/src/core/types.ts
index 4883d4068a..fccead863f 100644
--- a/src/core/types.ts
+++ b/src/core/types.ts
@@ -105,6 +105,12 @@ export interface QueryObserverConfig<
* Defaults to `true`.
*/
refetchOnMount?: boolean
+ /**
+ * Whether a change to the query status should re-render a component.
+ * If set to `false`, the component will only re-render when the actual `data` or `error` changes.
+ * Defaults to `true`.
+ */
+ notifyOnStatusChange?: boolean
/**
* This callback will fire any time the query successfully fetches new data.
*/
diff --git a/src/react/tests/useInfiniteQuery.test.tsx b/src/react/tests/useInfiniteQuery.test.tsx
index b5d3552cb8..86e0d06777 100644
--- a/src/react/tests/useInfiniteQuery.test.tsx
+++ b/src/react/tests/useInfiniteQuery.test.tsx
@@ -60,7 +60,7 @@ describe('useInfiniteQuery', () => {
await waitFor(() => rendered.getByText('Status: success'))
expect(states[0]).toEqual({
- canFetchmore: undefined,
+ canFetchMore: undefined,
clear: expect.any(Function),
data: undefined,
error: null,
diff --git a/src/react/tests/useQuery.test.tsx b/src/react/tests/useQuery.test.tsx
index 2307b4efe9..be32275c1c 100644
--- a/src/react/tests/useQuery.test.tsx
+++ b/src/react/tests/useQuery.test.tsx
@@ -7,6 +7,7 @@ import {
queryKey,
mockVisibilityState,
mockConsoleError,
+ waitForMs,
} from './utils'
import { useQuery } from '..'
import { queryCache, QueryResult } from '../../core'
@@ -468,7 +469,7 @@ describe('useQuery', () => {
await queryCache.prefetchQuery(key, () => 'prefetch')
- await sleep(10)
+ await sleep(40)
function FirstComponent() {
const state = useQuery(key, () => 'one', {
@@ -480,7 +481,7 @@ describe('useQuery', () => {
function SecondComponent() {
const state = useQuery(key, () => 'two', {
- staleTime: 5,
+ staleTime: 20,
})
states2.push(state)
return null
@@ -543,6 +544,76 @@ describe('useQuery', () => {
})
})
+ it('should re-render when a query becomes stale', async () => {
+ const key = queryKey()
+ const states: QueryResult[] = []
+
+ function Page() {
+ const state = useQuery(key, () => 'test', {
+ staleTime: 50,
+ })
+ states.push(state)
+ return null
+ }
+
+ render()
+
+ await waitFor(() => expect(states.length).toBe(3))
+
+ expect(states[0]).toMatchObject({
+ isStale: true,
+ })
+ expect(states[1]).toMatchObject({
+ isStale: false,
+ })
+ expect(states[2]).toMatchObject({
+ isStale: true,
+ })
+ })
+
+ it('should not re-render when a query status changes and notifyOnStatusChange is false', async () => {
+ const key = queryKey()
+ const states: QueryResult[] = []
+
+ function Page() {
+ const state = useQuery(
+ key,
+ async () => {
+ await sleep(5)
+ return 'test'
+ },
+ {
+ notifyOnStatusChange: false,
+ }
+ )
+
+ states.push(state)
+
+ const { refetch } = state
+
+ React.useEffect(() => {
+ setTimeout(refetch, 10)
+ }, [refetch])
+ return null
+ }
+
+ render()
+
+ await waitForMs(30)
+
+ expect(states.length).toBe(2)
+ expect(states[0]).toMatchObject({
+ data: undefined,
+ status: 'loading',
+ isFetching: true,
+ })
+ expect(states[1]).toMatchObject({
+ data: 'test',
+ status: 'success',
+ isFetching: false,
+ })
+ })
+
// See https://github.com/tannerlinsley/react-query/issues/137
it('should not override initial data in dependent queries', async () => {
const key1 = queryKey()
diff --git a/src/react/useMutation.ts b/src/react/useMutation.ts
index 2a641f2baf..349cfbaccf 100644
--- a/src/react/useMutation.ts
+++ b/src/react/useMutation.ts
@@ -172,8 +172,6 @@ export function useMutation<
if (mutateConfig.throwOnError ?? config.throwOnError) {
throw error
}
-
- return
}
},
[dispatch, getConfig, getMutationFn]
diff --git a/tsconfig.json b/tsconfig.json
index 5d737de6c9..287685b3f9 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -6,7 +6,7 @@
"target": "es5",
"noEmit": true,
"noImplicitAny": true,
- "noImplicitReturns": true,
+ "noImplicitReturns": false,
"noImplicitThis": true,
"noUnusedLocals": true,
"noUnusedParameters": true,