Skip to content

Commit

Permalink
feat: add notifyOnStatusChange flag
Browse files Browse the repository at this point in the history
  • Loading branch information
boschni committed Aug 26, 2020
1 parent 8f6bdf3 commit 1278dfc
Show file tree
Hide file tree
Showing 11 changed files with 193 additions and 59 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
1 change: 1 addition & 0 deletions src/core/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,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 @@ -469,7 +469,7 @@ export class Query<TResult, TError> {
const 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 @@ -313,7 +313,6 @@ export class QueryCache {
if (options?.throwOnError) {
throw error
}
return
} finally {
if (query) {
// When prefetching, no observer is tied to the query,
Expand Down
68 changes: 38 additions & 30 deletions src/core/queryObserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,39 +157,32 @@ export class QueryObserver<TResult, TError> {

private createResult(): QueryResult<TResult, TError> {
const { currentQuery, previousResult, config } = this

const {
canFetchMore,
error,
failureCount,
isFetched,
isFetching,
isFetchingMore,
isLoading,
isStale,
} = currentQuery.state

let { data, status, updatedAt } = currentQuery.state
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
}

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,
isStale,
isFetched: state.isFetched,
isFetching: state.isFetching,
isFetchingMore: state.isFetchingMore,
isStale: state.isStale,
query: currentQuery,
refetch: this.refetch,
updatedAt,
Expand Down Expand Up @@ -229,20 +222,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.updateRefetchInterval()
} 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.updateRefetchInterval()
}

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
105 changes: 105 additions & 0 deletions src/react/tests/useQuery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,111 @@ 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()

let count = 0
const fn = () => sleep(10).then(() => count++)

const states1: QueryResult<number>[] = []
const states2: QueryResult<number>[] = []

function FirstCounter() {
const state = useQuery(key, fn, {
notifyOnStatusChange: false,
})
states1.push(state)
return null
}

function SecondCounter() {
const state = useQuery(key, fn)
states2.push(state)
return null
}

function Footer() {
const [showCounter, setShowCounter] = React.useState(false)

React.useEffect(() => {
setTimeout(() => {
setShowCounter(true)
}, 20)
}, [])

return <div>{showCounter && <SecondCounter />}</div>
}

function Page() {
return (
<div>
<FirstCounter />
<Footer />
</div>
)
}

render(<Page />)

await waitFor(() => expect(states1[2]?.data).toBe(1))

expect(states1.length).toBe(3)

expect(states1[0]).toMatchObject({
status: 'loading',
isFetching: true,
})
expect(states1[1]).toMatchObject({
status: 'success',
isFetching: false,
})
expect(states1[2]).toMatchObject({
status: 'success',
isFetching: false,
})

expect(states2.length).toBe(3)
expect(states2[0]).toMatchObject({
status: 'success',
isFetching: false,
})
expect(states2[1]).toMatchObject({
status: 'success',
isFetching: true,
})
expect(states2[2]).toMatchObject({
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
11 changes: 11 additions & 0 deletions src/react/tests/utils.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { waitFor } from '@testing-library/react'

let queryKeyCount = 0

export function mockVisibilityState(value: string) {
Expand Down Expand Up @@ -31,6 +33,15 @@ export function sleep(timeout: number): Promise<void> {
})
}

export function waitForMs(ms: number) {
const end = Date.now() + ms
return waitFor(() => {
if (Date.now() < end) {
throw new Error('Time not elapsed yet')
}
})
}

/**
* Checks that `T` is of type `U`.
*/
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 1278dfc

Please sign in to comment.