Skip to content

Commit

Permalink
feat: add notifyOnStatusChange flag (TanStack#840)
Browse files Browse the repository at this point in the history
  • Loading branch information
boschni committed Sep 2, 2020
1 parent 52607f6 commit b3c9ce8
Show file tree
Hide file tree
Showing 11 changed files with 152 additions and 61 deletions.
52 changes: 29 additions & 23 deletions docs/src/pages/docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})

Expand Down Expand Up @@ -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.
Expand Down
3 changes: 2 additions & 1 deletion docs/src/pages/docs/comparison.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 ||||
Expand All @@ -38,6 +37,8 @@ Feature/Capability Key:
| Prefetching APIs || 🔶 ||
| Query Cancellation || 🛑 | 🛑 |
| Partial Query Matching<sup>2</sup> || 🛑 | 🛑 |
| Stale While Revalidate ||| 🛑 |
| Stale Time Configuration || 🛑 | 🛑 |
| Window Focus Refetching ||| 🛑 |
| Network Status Refetching ||||
| Automatic Refetch after Mutation<sup>3</sup> | 🔶 | 🔶 ||
Expand Down
1 change: 1 addition & 0 deletions src/core/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export const DEFAULT_CONFIG: ReactQueryConfig = {
refetchOnWindowFocus: true,
refetchOnReconnect: true,
refetchOnMount: true,
notifyOnStatusChange: true,
structuralSharing: true,
},
}
Expand Down
2 changes: 1 addition & 1 deletion src/core/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ export class Query<TResult, TError> {
config = this.config

// Check if there is a query function
if (!config.queryFn) {
if (typeof config.queryFn !== 'function') {
return
}

Expand Down
1 change: 0 additions & 1 deletion src/core/queryCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,6 @@ export class QueryCache {
if (options?.throwOnError) {
throw error
}
return
}
}

Expand Down
67 changes: 38 additions & 29 deletions src/core/queryObserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,22 +217,16 @@ export class QueryObserver<TResult, TError> {
}

private createResult(): QueryResult<TResult, TError> {
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
Expand All @@ -256,15 +250,15 @@ export class QueryObserver<TResult, TError> {

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,
Expand Down Expand Up @@ -304,20 +298,35 @@ export class QueryObserver<TResult, TError> {
_state: QueryState<TResult, TError>,
action: Action<TResult, TError>
): 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)
}
}
}
6 changes: 6 additions & 0 deletions src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
2 changes: 1 addition & 1 deletion src/react/tests/useInfiniteQuery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
75 changes: 73 additions & 2 deletions src/react/tests/useQuery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
queryKey,
mockVisibilityState,
mockConsoleError,
waitForMs,
} from './utils'
import { useQuery } from '..'
import { queryCache, QueryResult } from '../../core'
Expand Down Expand Up @@ -518,7 +519,7 @@ describe('useQuery', () => {

await queryCache.prefetchQuery(key, () => 'prefetch')

await sleep(10)
await sleep(40)

function FirstComponent() {
const state = useQuery(key, () => 'one', {
Expand All @@ -530,7 +531,7 @@ describe('useQuery', () => {

function SecondComponent() {
const state = useQuery(key, () => 'two', {
staleTime: 5,
staleTime: 20,
})
states2.push(state)
return null
Expand Down Expand Up @@ -593,6 +594,76 @@ describe('useQuery', () => {
})
})

it('should re-render when a query becomes stale', async () => {
const key = queryKey()
const states: QueryResult<string>[] = []

function Page() {
const state = useQuery(key, () => 'test', {
staleTime: 50,
})
states.push(state)
return null
}

render(<Page />)

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<string>[] = []

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(<Page />)

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()
Expand Down
2 changes: 0 additions & 2 deletions src/react/useMutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,6 @@ export function useMutation<
if (mutateConfig.throwOnError ?? config.throwOnError) {
throw error
}

return
}
},
[dispatch, getConfig, getMutationFn]
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"target": "es5",
"noEmit": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"noImplicitReturns": false,
"noImplicitThis": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
Expand Down

0 comments on commit b3c9ce8

Please sign in to comment.