Skip to content

Commit 1c73403

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

File tree

11 files changed

+402
-84
lines changed

11 files changed

+402
-84
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: 63 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import {
22
isServer,
33
functionalUpdate,
4-
cancelledError,
54
isDocumentVisible,
65
noop,
76
Console,
@@ -104,6 +103,8 @@ export type Action<TResult, TError> =
104103
| SetStateAction<TResult, TError>
105104
| SuccessAction<TResult>
106105

106+
class CancelledError extends Error {}
107+
107108
// CLASS
108109

109110
export class Query<TResult, TError> {
@@ -122,7 +123,7 @@ export class Query<TResult, TError> {
122123
private retryTimeout?: number
123124
private staleTimeout?: number
124125
private cancelPromises?: () => void
125-
private cancelled?: typeof cancelledError | null
126+
private cancelled?: boolean
126127
private notifyGlobalListeners: (query: Query<TResult, TError>) => void
127128

128129
constructor(init: QueryInitConfig<TResult, TError>) {
@@ -157,12 +158,13 @@ export class Query<TResult, TError> {
157158
}
158159

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

162164
// Only update state if something has changed
163-
if (!shallowEqual(this.state, newState)) {
165+
if (!shallowEqual(prevState, newState)) {
164166
this.state = newState
165-
this.instances.forEach(d => d.onStateUpdate(newState, action))
167+
this.instances.forEach(d => d.onStateUpdate(newState, prevState, action))
166168
this.notifyGlobalListeners(this)
167169
}
168170
}
@@ -236,11 +238,11 @@ export class Query<TResult, TError> {
236238
this.clearCacheTimeout()
237239

238240
// Mark the query as not cancelled
239-
this.cancelled = null
241+
this.cancelled = false
240242
}
241243

242244
cancel(): void {
243-
this.cancelled = cancelledError
245+
this.cancelled = true
244246

245247
if (this.cancelPromises) {
246248
this.cancelPromises()
@@ -322,21 +324,30 @@ export class Query<TResult, TError> {
322324
args: ArrayQueryKey
323325
): Promise<TResult> {
324326
try {
327+
const filter = this.config.queryFnParamsFilter
328+
const params = filter ? filter(args) : args
329+
325330
// Perform the query
326-
const promiseOrValue = fn(...this.config.queryFnParamsFilter!(args))
331+
const promiseOrValue = fn(...params)
327332

328333
this.cancelPromises = () => (promiseOrValue as any)?.cancel?.()
329334

330335
const data = await promiseOrValue
331336
delete this.shouldContinueRetryOnFocus
332337

333338
delete this.cancelPromises
334-
if (this.cancelled) throw this.cancelled
339+
340+
if (this.cancelled) {
341+
throw new CancelledError()
342+
}
335343

336344
return data
337345
} catch (error) {
338346
delete this.cancelPromises
339-
if (this.cancelled) throw this.cancelled
347+
348+
if (this.cancelled) {
349+
throw new CancelledError()
350+
}
340351

341352
// Do we need to retry the request?
342353
if (
@@ -368,14 +379,23 @@ export class Query<TResult, TError> {
368379
return await new Promise((resolve, reject) => {
369380
// Keep track of the retry timeout
370381
this.retryTimeout = setTimeout(async () => {
371-
if (this.cancelled) return reject(this.cancelled)
382+
if (this.cancelled) {
383+
return reject(new CancelledError())
384+
}
372385

373386
try {
374387
const data = await this.tryFetchData(fn, args)
375-
if (this.cancelled) return reject(this.cancelled)
388+
389+
if (this.cancelled) {
390+
return reject(new CancelledError())
391+
}
392+
376393
resolve(data)
377394
} catch (error) {
378-
if (this.cancelled) return reject(this.cancelled)
395+
if (this.cancelled) {
396+
return reject(new CancelledError())
397+
}
398+
379399
reject(error)
380400
}
381401
}, delay)
@@ -499,7 +519,7 @@ export class Query<TResult, TError> {
499519

500520
this.promise = (async () => {
501521
// If there are any retries pending for this query, kill them
502-
this.cancelled = null
522+
this.cancelled = false
503523

504524
try {
505525
// Set up the query refreshing state
@@ -514,15 +534,17 @@ export class Query<TResult, TError> {
514534

515535
return data
516536
} catch (error) {
537+
const cancelled = error instanceof CancelledError
538+
517539
this.dispatch({
518540
type: ActionType.Error,
519-
cancelled: error === this.cancelled,
541+
cancelled,
520542
error,
521543
})
522544

523545
delete this.promise
524546

525-
if (error !== this.cancelled) {
547+
if (!cancelled) {
526548
throw error
527549
}
528550

@@ -556,11 +578,31 @@ function getDefaultState<TResult, TError>(
556578

557579
const hasInitialData = typeof initialData !== 'undefined'
558580

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

565607
const initialStatus = hasInitialData
566608
? QueryStatus.Success

src/core/queryInstance.ts

Lines changed: 45 additions & 6 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
@@ -104,18 +104,57 @@ export class QueryInstance<TResult, TError> {
104104

105105
onStateUpdate(
106106
state: QueryState<TResult, TError>,
107+
prevState: QueryState<TResult, TError>,
107108
action: Action<TResult, TError>
108109
): void {
110+
// Trigger callbacks on a success event which resulted in a success state
109111
if (action.type === ActionType.Success && state.isSuccess) {
110112
this.config.onSuccess?.(state.data!)
111113
this.config.onSettled?.(state.data!, null)
112114
}
113115

116+
// Trigger callbacks on an error event which resulted in an error state
114117
if (action.type === ActionType.Error && state.isError) {
115118
this.config.onError?.(state.error!)
116119
this.config.onSettled?.(undefined, state.error!)
117120
}
118121

119-
this.stateUpdateListener?.(state)
122+
// Check if we need to notify the subscriber
123+
let notify = false
124+
125+
// Always notify on data and error changes
126+
if (state.data !== prevState.data || state.error !== prevState.error) {
127+
notify = true
128+
}
129+
130+
// Maybe notify on status changes
131+
if (
132+
this.config.notifyOnStatusChange &&
133+
(state.status !== prevState.status ||
134+
state.isFetching !== prevState.isFetching ||
135+
state.isFetchingMore !== prevState.isFetchingMore)
136+
) {
137+
notify = true
138+
}
139+
140+
// Maybe notify on failureCount changes
141+
if (
142+
this.config.notifyOnFailureCountChange &&
143+
state.failureCount !== prevState.failureCount
144+
) {
145+
notify = true
146+
}
147+
148+
// Maybe notify on stale changes
149+
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)