Skip to content

Commit 0567def

Browse files
committed
feat: add notify flags for controlling re-renders
1 parent 58acca1 commit 0567def

File tree

11 files changed

+420
-88
lines changed

11 files changed

+420
-88
lines changed

docs/src/pages/docs/api.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ const {
3333
queryFnParamsFilter,
3434
refetchOnMount,
3535
isDataEqual,
36+
notifyOnFailureCountChange,
37+
notifyOnStaleChange,
38+
notifyOnStatusChange,
3639
onError,
3740
onSuccess,
3841
onSettled,
@@ -90,6 +93,18 @@ const queryInfo = useQuery({
9093
- `refetchOnWindowFocus: Boolean`
9194
- Optional
9295
- Set this to `true` or `false` to enable/disable automatic refetching on window focus for this query.
96+
- `notifyOnFailureCountChange: Boolean`
97+
- Optional
98+
- Defaults to `true`
99+
- Whether components should re-render when a query failure count changes.
100+
- `notifyOnStaleChange: Boolean`
101+
- Optional
102+
- Defaults to `true`
103+
- Whether components should re-render when a query becomes stale.
104+
- `notifyOnStatusChange: Boolean`
105+
- Optional
106+
- Defaults to `true`
107+
- Whether components should re-render when a query status changes. This includes the `isFetching` and `isFetchingMore` states.
93108
- `onSuccess: Function(data) => data`
94109
- Optional
95110
- This function will fire any time the query successfully fetches new data.

src/core/config.ts

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { stableStringify, identity, deepEqual } from './utils'
1+
import { stableStringify, deepEqual } from './utils'
22
import {
33
ArrayQueryKey,
44
QueryKey,
@@ -30,26 +30,19 @@ export const defaultQueryKeySerializerFn: QueryKeySerializerFunction = (
3030
}
3131

3232
export const DEFAULT_CONFIG: ReactQueryConfig = {
33-
shared: {
34-
suspense: false,
35-
},
3633
queries: {
3734
queryKeySerializerFn: defaultQueryKeySerializerFn,
3835
enabled: true,
3936
retry: 3,
4037
retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
4138
staleTime: 0,
4239
cacheTime: 5 * 60 * 1000,
40+
notifyOnFailureCountChange: true,
41+
notifyOnStaleChange: true,
42+
notifyOnStatusChange: true,
4343
refetchOnWindowFocus: true,
44-
refetchInterval: false,
45-
queryFnParamsFilter: identity,
4644
refetchOnMount: true,
4745
isDataEqual: deepEqual,
48-
useErrorBoundary: false,
49-
},
50-
mutations: {
51-
throwOnError: false,
52-
useErrorBoundary: false,
5346
},
5447
}
5548

src/core/query.ts

Lines changed: 70 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {
2+
isCancelable,
23
isServer,
34
functionalUpdate,
4-
cancelledError,
55
isDocumentVisible,
66
noop,
77
Console,
@@ -104,6 +104,8 @@ export type Action<TResult, TError> =
104104
| SetStateAction<TResult, TError>
105105
| SuccessAction<TResult>
106106

107+
class CancelledError extends Error {}
108+
107109
// CLASS
108110

109111
export class Query<TResult, TError> {
@@ -122,7 +124,7 @@ export class Query<TResult, TError> {
122124
private retryTimeout?: number
123125
private staleTimeout?: number
124126
private cancelPromises?: () => void
125-
private cancelled?: typeof cancelledError | null
127+
private cancelled?: boolean
126128
private notifyGlobalListeners: (query: Query<TResult, TError>) => void
127129

128130
constructor(init: QueryInitConfig<TResult, TError>) {
@@ -157,12 +159,13 @@ export class Query<TResult, TError> {
157159
}
158160

159161
private dispatch(action: Action<TResult, TError>): void {
160-
const newState = queryReducer(this.state, action)
162+
const prevState = this.state
163+
const newState = queryReducer(prevState, action)
161164

162165
// Only update state if something has changed
163-
if (!shallowEqual(this.state, newState)) {
166+
if (!shallowEqual(prevState, newState)) {
164167
this.state = newState
165-
this.instances.forEach(d => d.onStateUpdate(newState, action))
168+
this.instances.forEach(d => d.onStateUpdate(newState, prevState, action))
166169
this.notifyGlobalListeners(this)
167170
}
168171
}
@@ -236,11 +239,15 @@ export class Query<TResult, TError> {
236239
this.clearCacheTimeout()
237240

238241
// Mark the query as not cancelled
239-
this.cancelled = null
242+
this.cancelled = false
243+
}
244+
245+
canCancel(): boolean {
246+
return !this.cancelled && Boolean(this.cancelPromises)
240247
}
241248

242249
cancel(): void {
243-
this.cancelled = cancelledError
250+
this.cancelled = true
244251

245252
if (this.cancelPromises) {
246253
this.cancelPromises()
@@ -322,21 +329,32 @@ export class Query<TResult, TError> {
322329
args: ArrayQueryKey
323330
): Promise<TResult> {
324331
try {
332+
const filter = this.config.queryFnParamsFilter
333+
const params = filter ? filter(args) : args
334+
325335
// Perform the query
326-
const promiseOrValue = fn(...this.config.queryFnParamsFilter!(args))
336+
const promiseOrValue = fn(...params)
327337

328-
this.cancelPromises = () => (promiseOrValue as any)?.cancel?.()
338+
if (isCancelable(promiseOrValue)) {
339+
this.cancelPromises = () => promiseOrValue.cancel()
340+
}
329341

330342
const data = await promiseOrValue
331343
delete this.shouldContinueRetryOnFocus
332344

333345
delete this.cancelPromises
334-
if (this.cancelled) throw this.cancelled
346+
347+
if (this.cancelled) {
348+
throw new CancelledError()
349+
}
335350

336351
return data
337352
} catch (error) {
338353
delete this.cancelPromises
339-
if (this.cancelled) throw this.cancelled
354+
355+
if (this.cancelled) {
356+
throw new CancelledError()
357+
}
340358

341359
// Do we need to retry the request?
342360
if (
@@ -368,14 +386,23 @@ export class Query<TResult, TError> {
368386
return await new Promise((resolve, reject) => {
369387
// Keep track of the retry timeout
370388
this.retryTimeout = setTimeout(async () => {
371-
if (this.cancelled) return reject(this.cancelled)
389+
if (this.cancelled) {
390+
return reject(new CancelledError())
391+
}
372392

373393
try {
374394
const data = await this.tryFetchData(fn, args)
375-
if (this.cancelled) return reject(this.cancelled)
395+
396+
if (this.cancelled) {
397+
return reject(new CancelledError())
398+
}
399+
376400
resolve(data)
377401
} catch (error) {
378-
if (this.cancelled) return reject(this.cancelled)
402+
if (this.cancelled) {
403+
return reject(new CancelledError())
404+
}
405+
379406
reject(error)
380407
}
381408
}, delay)
@@ -499,7 +526,7 @@ export class Query<TResult, TError> {
499526

500527
this.promise = (async () => {
501528
// If there are any retries pending for this query, kill them
502-
this.cancelled = null
529+
this.cancelled = false
503530

504531
try {
505532
// Set up the query refreshing state
@@ -514,15 +541,17 @@ export class Query<TResult, TError> {
514541

515542
return data
516543
} catch (error) {
544+
const cancelled = error instanceof CancelledError
545+
517546
this.dispatch({
518547
type: ActionType.Error,
519-
cancelled: error === this.cancelled,
548+
cancelled,
520549
error,
521550
})
522551

523552
delete this.promise
524553

525-
if (error !== this.cancelled) {
554+
if (!cancelled) {
526555
throw error
527556
}
528557

@@ -556,11 +585,30 @@ function getDefaultState<TResult, TError>(
556585

557586
const hasInitialData = typeof initialData !== 'undefined'
558587

559-
const isStale =
560-
!config.enabled ||
561-
(typeof config.initialStale === 'function'
562-
? config.initialStale()
563-
: config.initialStale ?? !hasInitialData)
588+
// A query is stale by default
589+
let isStale = true
590+
591+
if (hasInitialData) {
592+
// When initial data is provided, the query is not stale by default
593+
isStale = false
594+
595+
// Mark the query as stale if initialStale is set to `true`
596+
if (config.initialStale === true) {
597+
isStale = true
598+
}
599+
// Mark the query as stale if initialStale is set to a function which returns `true`
600+
else if (
601+
typeof config.initialStale === 'function' &&
602+
config.initialStale()
603+
) {
604+
isStale = true
605+
}
606+
}
607+
608+
// Always mark the query as stale when it is not enabled
609+
if (!config.enabled) {
610+
isStale = true
611+
}
564612

565613
const initialStatus = hasInitialData
566614
? QueryStatus.Success

src/core/queryInstance.ts

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { uid, isServer, isDocumentVisible, Console } from './utils'
22
import { Query, QueryState, Action, ActionType } from './query'
3-
import { BaseQueryConfig } from './types'
3+
import { BaseQueryInstanceConfig } from './types'
44

55
// TYPES
66

@@ -12,20 +12,20 @@ export type OnStateUpdateFunction<TResult, TError> = (
1212

1313
export class QueryInstance<TResult, TError> {
1414
id: number
15-
config: BaseQueryConfig<TResult, TError>
15+
config: BaseQueryInstanceConfig<TResult, TError>
1616

1717
private query: Query<TResult, TError>
1818
private refetchIntervalId?: number
1919
private stateUpdateListener?: OnStateUpdateFunction<TResult, TError>
2020

2121
constructor(
2222
query: Query<TResult, TError>,
23-
onStateUpdate?: OnStateUpdateFunction<TResult, TError>
23+
stateUpdateListener?: OnStateUpdateFunction<TResult, TError>
2424
) {
2525
this.id = uid()
26-
this.stateUpdateListener = onStateUpdate
2726
this.query = query
2827
this.config = {}
28+
this.stateUpdateListener = stateUpdateListener
2929
}
3030

3131
clearInterval(): void {
@@ -35,7 +35,7 @@ export class QueryInstance<TResult, TError> {
3535
}
3636
}
3737

38-
updateConfig(config: BaseQueryConfig<TResult, TError>): void {
38+
updateConfig(config: BaseQueryInstanceConfig<TResult, TError>): void {
3939
const oldConfig = this.config
4040

4141
// Update the config
@@ -93,7 +93,11 @@ export class QueryInstance<TResult, TError> {
9393

9494
if (!this.query.instances.length) {
9595
this.clearInterval()
96-
this.query.cancel()
96+
97+
// Only cancel the query if the transport layer supports cancellation
98+
if (this.query.canCancel()) {
99+
this.query.cancel()
100+
}
97101

98102
if (!preventGC && !isServer) {
99103
// Schedule garbage collection
@@ -104,18 +108,53 @@ export class QueryInstance<TResult, TError> {
104108

105109
onStateUpdate(
106110
state: QueryState<TResult, TError>,
111+
prevState: QueryState<TResult, TError>,
107112
action: Action<TResult, TError>
108113
): void {
114+
// Trigger callbacks on a success event which resulted in a success state
109115
if (action.type === ActionType.Success && state.isSuccess) {
110116
this.config.onSuccess?.(state.data!)
111117
this.config.onSettled?.(state.data!, null)
112118
}
113-
114-
if (action.type === ActionType.Error && state.isError) {
119+
// Trigger callbacks on an error event which resulted in an error state
120+
else if (action.type === ActionType.Error && state.isError) {
115121
this.config.onError?.(state.error!)
116122
this.config.onSettled?.(undefined, state.error!)
117123
}
118124

119-
this.stateUpdateListener?.(state)
125+
// Check if we need to notify the subscriber
126+
let notify = false
127+
128+
// Always notify on data and error changes
129+
if (state.data !== prevState.data || state.error !== prevState.error) {
130+
notify = true
131+
}
132+
// Maybe notify on status changes
133+
else if (
134+
this.config.notifyOnStatusChange &&
135+
(state.status !== prevState.status ||
136+
state.isFetching !== prevState.isFetching ||
137+
state.isFetchingMore !== prevState.isFetchingMore)
138+
) {
139+
notify = true
140+
}
141+
// Maybe notify on failureCount changes
142+
else if (
143+
this.config.notifyOnFailureCountChange &&
144+
state.failureCount !== prevState.failureCount
145+
) {
146+
notify = true
147+
}
148+
// Maybe notify on stale changes
149+
else if (
150+
this.config.notifyOnStaleChange &&
151+
state.isStale !== prevState.isStale
152+
) {
153+
notify = true
154+
}
155+
156+
if (notify) {
157+
this.stateUpdateListener?.(state)
158+
}
120159
}
121160
}

0 commit comments

Comments
 (0)