Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add notify flag for controlling re-renders #840

Merged
merged 1 commit into from
Sep 2, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 | ✅ | 🛑 | 🛑 |
tannerlinsley marked this conversation as resolved.
Show resolved Hide resolved
| 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 @@ -409,7 +409,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
}
}

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 @@ -303,20 +297,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 @@ -468,7 +469,7 @@ describe('useQuery', () => {

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

await sleep(10)
await sleep(40)

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

function SecondComponent() {
const state = useQuery(key, () => 'two', {
staleTime: 5,
staleTime: 20,
})
states2.push(state)
return null
Expand Down Expand Up @@ -543,6 +544,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