Skip to content

Commit 140515d

Browse files
authored
fix: make sure all callbacks are called in suspense mode (#828)
1 parent 880f971 commit 140515d

File tree

9 files changed

+95
-73
lines changed

9 files changed

+95
-73
lines changed

src/core/query.ts

Lines changed: 7 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export interface QueryState<TResult, TError> {
3737
error: TError | null
3838
failureCount: number
3939
isError: boolean
40+
isFetched: boolean
4041
isFetching: boolean
4142
isFetchingMore?: IsFetchingMoreValue
4243
isIdle: boolean
@@ -57,7 +58,7 @@ export interface FetchMoreOptions {
5758
previous: boolean
5859
}
5960

60-
enum ActionType {
61+
export enum ActionType {
6162
Failed = 'Failed',
6263
MarkStale = 'MarkStale',
6364
Fetch = 'Fetch',
@@ -95,7 +96,7 @@ interface SetStateAction<TResult, TError> {
9596
updater: Updater<QueryState<TResult, TError>, QueryState<TResult, TError>>
9697
}
9798

98-
type Action<TResult, TError> =
99+
export type Action<TResult, TError> =
99100
| ErrorAction<TError>
100101
| FailedAction
101102
| FetchAction
@@ -112,8 +113,6 @@ export class Query<TResult, TError> {
112113
config: QueryConfig<TResult, TError>
113114
instances: QueryInstance<TResult, TError>[]
114115
state: QueryState<TResult, TError>
115-
fallbackInstance?: QueryInstance<TResult, TError>
116-
wasSuspended?: boolean
117116
shouldContinueRetryOnFocus?: boolean
118117
promise?: Promise<TResult | undefined>
119118

@@ -163,7 +162,7 @@ export class Query<TResult, TError> {
163162
// Only update state if something has changed
164163
if (!shallowEqual(this.state, newState)) {
165164
this.state = newState
166-
this.instances.forEach(d => d.onStateUpdate?.(this.state))
165+
this.instances.forEach(d => d.onStateUpdate(newState, action))
167166
this.notifyGlobalListeners(this)
168167
}
169168
}
@@ -502,15 +501,6 @@ export class Query<TResult, TError> {
502501
// If there are any retries pending for this query, kill them
503502
this.cancelled = null
504503

505-
const getCallbackInstances = () => {
506-
const callbackInstances = [...this.instances]
507-
508-
if (this.wasSuspended && this.fallbackInstance) {
509-
callbackInstances.unshift(this.fallbackInstance)
510-
}
511-
return callbackInstances
512-
}
513-
514504
try {
515505
// Set up the query refreshing state
516506
this.dispatch({ type: ActionType.Fetch })
@@ -520,14 +510,6 @@ export class Query<TResult, TError> {
520510

521511
this.setData(old => (this.config.isDataEqual!(old, data) ? old! : data))
522512

523-
getCallbackInstances().forEach(instance => {
524-
instance.config.onSuccess?.(this.state.data!)
525-
})
526-
527-
getCallbackInstances().forEach(instance =>
528-
instance.config.onSettled?.(this.state.data, null)
529-
)
530-
531513
delete this.promise
532514

533515
return data
@@ -541,14 +523,6 @@ export class Query<TResult, TError> {
541523
delete this.promise
542524

543525
if (error !== this.cancelled) {
544-
getCallbackInstances().forEach(instance =>
545-
instance.config.onError?.(error)
546-
)
547-
548-
getCallbackInstances().forEach(instance =>
549-
instance.config.onSettled?.(undefined, error)
550-
)
551-
552526
throw error
553527
}
554528

@@ -597,6 +571,7 @@ function getDefaultState<TResult, TError>(
597571
return {
598572
...getStatusProps(initialStatus),
599573
error: null,
574+
isFetched: false,
600575
isFetching: initialStatus === QueryStatus.Loading,
601576
failureCount: 0,
602577
isStale,
@@ -638,6 +613,7 @@ export function queryReducer<TResult, TError>(
638613
data: functionalUpdate(action.updater, state.data),
639614
error: null,
640615
isStale: action.isStale,
616+
isFetched: true,
641617
isFetching: false,
642618
updatedAt: Date.now(),
643619
failureCount: 0,
@@ -646,6 +622,7 @@ export function queryReducer<TResult, TError>(
646622
return {
647623
...state,
648624
failureCount: state.failureCount + 1,
625+
isFetched: true,
649626
isFetching: false,
650627
isStale: true,
651628
...(!action.cancelled && {

src/core/queryCache.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import {
1818
TupleQueryFunction,
1919
TupleQueryKey,
2020
} from './types'
21-
import { QueryInstance } from './queryInstance'
2221

2322
// TYPES
2423

@@ -283,14 +282,6 @@ export class QueryCache {
283282
}
284283
}
285284

286-
query.fallbackInstance = {
287-
config: {
288-
onSuccess: query.config.onSuccess,
289-
onError: query.config.onError,
290-
onSettled: query.config.onSettled,
291-
},
292-
} as QueryInstance<TResult, TError>
293-
294285
return query
295286
}
296287

src/core/queryInstance.ts

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

55
// TYPES
@@ -13,17 +13,17 @@ export type OnStateUpdateFunction<TResult, TError> = (
1313
export class QueryInstance<TResult, TError> {
1414
id: number
1515
config: BaseQueryConfig<TResult, TError>
16-
onStateUpdate?: OnStateUpdateFunction<TResult, TError>
1716

1817
private query: Query<TResult, TError>
1918
private refetchIntervalId?: number
19+
private stateUpdateListener?: OnStateUpdateFunction<TResult, TError>
2020

2121
constructor(
2222
query: Query<TResult, TError>,
2323
onStateUpdate?: OnStateUpdateFunction<TResult, TError>
2424
) {
2525
this.id = uid()
26-
this.onStateUpdate = onStateUpdate
26+
this.stateUpdateListener = onStateUpdate
2727
this.query = query
2828
this.config = {}
2929
}
@@ -77,30 +77,45 @@ export class QueryInstance<TResult, TError> {
7777
// Perform the refetch for this query if necessary
7878
if (
7979
this.query.instances.some(d => d.config.enabled) && // Don't auto refetch if disabled
80-
!this.query.wasSuspended && // Don't double refetch for suspense
80+
!(this.config.suspense && this.query.state.isFetched) && // Don't refetch if in suspense mode and the data is already fetched
8181
this.query.state.isStale && // Only refetch if stale
82-
(this.query.config.refetchOnMount || this.query.instances.length === 1)
82+
(this.config.refetchOnMount || this.query.instances.length === 1)
8383
) {
8484
await this.query.fetch()
8585
}
86-
87-
this.query.wasSuspended = false
8886
} catch (error) {
8987
Console.error(error)
9088
}
9189
}
9290

93-
unsubscribe(): void {
91+
unsubscribe(preventGC?: boolean): void {
9492
this.query.instances = this.query.instances.filter(d => d.id !== this.id)
9593

9694
if (!this.query.instances.length) {
9795
this.clearInterval()
9896
this.query.cancel()
9997

100-
if (!isServer) {
98+
if (!preventGC && !isServer) {
10199
// Schedule garbage collection
102100
this.query.scheduleGarbageCollection()
103101
}
104102
}
105103
}
104+
105+
onStateUpdate(
106+
state: QueryState<TResult, TError>,
107+
action: Action<TResult, TError>
108+
): void {
109+
if (action.type === ActionType.Success && state.isSuccess) {
110+
this.config.onSuccess?.(state.data!)
111+
this.config.onSettled?.(state.data!, null)
112+
}
113+
114+
if (action.type === ActionType.Error && state.isError) {
115+
this.config.onError?.(state.error!)
116+
this.config.onSettled?.(undefined, state.error!)
117+
}
118+
119+
this.stateUpdateListener?.(state)
120+
}
106121
}

src/react/tests/suspense.test.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,43 @@ describe("useQuery's in Suspense mode", () => {
9090
expect(successFn).toHaveBeenCalledTimes(1)
9191
})
9292

93+
it('should call every onSuccess handler within a suspense boundary', async () => {
94+
const key = queryKey()
95+
96+
const successFn1 = jest.fn()
97+
const successFn2 = jest.fn()
98+
99+
function FirstComponent() {
100+
useQuery(key, () => sleep(10), {
101+
suspense: true,
102+
onSuccess: successFn1,
103+
})
104+
105+
return <span>first</span>
106+
}
107+
108+
function SecondComponent() {
109+
useQuery(key, () => sleep(20), {
110+
suspense: true,
111+
onSuccess: successFn2,
112+
})
113+
114+
return <span>second</span>
115+
}
116+
117+
const rendered = render(
118+
<React.Suspense fallback="loading">
119+
<FirstComponent />
120+
<SecondComponent />
121+
</React.Suspense>
122+
)
123+
124+
await waitFor(() => rendered.getByText('second'))
125+
126+
expect(successFn1).toHaveBeenCalledTimes(1)
127+
expect(successFn1).toHaveBeenCalledTimes(1)
128+
})
129+
93130
// https://github.com/tannerlinsley/react-query/issues/468
94131
it('should reset error state if new component instances are mounted', async () => {
95132
const key = queryKey()

src/react/tests/useQuery.test.tsx

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -546,21 +546,16 @@ describe('useQuery', () => {
546546
staleTime: 10,
547547
})
548548

549-
await sleep(100)
549+
await sleep(11)
550550

551551
function Page() {
552-
const query = useQuery(key, queryFn)
553-
554-
return (
555-
<div>
556-
<div>{query.data}</div>
557-
</div>
558-
)
552+
useQuery(key, queryFn)
553+
return null
559554
}
560555

561556
render(<Page />)
562557

563-
await sleep(100)
558+
await act(() => sleep(0))
564559

565560
expect(prefetchQueryFn).toHaveBeenCalledTimes(1)
566561
expect(queryFn).toHaveBeenCalledTimes(1)
@@ -579,21 +574,16 @@ describe('useQuery', () => {
579574
staleTime: 1000,
580575
})
581576

582-
sleep(100)
577+
await sleep(0)
583578

584579
function Page() {
585-
const query = useQuery(key, queryFn)
586-
587-
return (
588-
<div>
589-
<div>{query.data}</div>
590-
</div>
591-
)
580+
useQuery(key, queryFn)
581+
return null
592582
}
593583

594584
render(<Page />)
595585

596-
sleep(100)
586+
await sleep(0)
597587

598588
expect(prefetchQueryFn).toHaveBeenCalledTimes(1)
599589
expect(queryFn).toHaveBeenCalledTimes(0)

src/react/useInfiniteQuery.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ export function useInfiniteQuery<TResult, TError>(
8080
const query = result.query
8181
const state = result.query.state
8282

83-
handleSuspense(result)
83+
handleSuspense(config, result)
8484

8585
const fetchMore = React.useMemo(() => query.fetchMore.bind(query), [query])
8686

src/react/usePaginatedQuery.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ export function usePaginatedQuery<TResult, TError>(
124124
Object.assign(result, overrides)
125125
}
126126

127-
handleSuspense(result)
127+
handleSuspense(config, result)
128128

129129
return {
130130
...result,

src/react/useQuery.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export function useQuery<TResult, TError>(
6666
const [queryKey, config] = useQueryArgs<TResult, TError>(args)
6767
const result = useBaseQuery<TResult, TError>(queryKey, config)
6868

69-
handleSuspense(result)
69+
handleSuspense(config, result)
7070

7171
return {
7272
...result,

src/react/utils.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from 'react'
22

33
import { uid, isServer } from '../core/utils'
4-
import { QueryResultBase, QueryStatus } from '../core/types'
4+
import { QueryResultBase, BaseQueryConfig, QueryStatus } from '../core/types'
55

66
export function useUid(): number {
77
const ref = React.useRef(0)
@@ -40,9 +40,12 @@ export function useRerenderer() {
4040
return React.useCallback(() => rerender({}), [rerender])
4141
}
4242

43-
export function handleSuspense(result: QueryResultBase<any, any>) {
43+
export function handleSuspense(
44+
config: BaseQueryConfig<any, any>,
45+
result: QueryResultBase<any, any>
46+
) {
4447
const { error, query } = result
45-
const { config, state } = query
48+
const { state } = query
4649

4750
if (config.suspense || config.useErrorBoundary) {
4851
if (state.status === QueryStatus.Error && state.throwInErrorBoundary) {
@@ -54,7 +57,16 @@ export function handleSuspense(result: QueryResultBase<any, any>) {
5457
state.status !== QueryStatus.Success &&
5558
config.enabled
5659
) {
57-
query.wasSuspended = true
60+
const instance = query.subscribe()
61+
62+
instance.updateConfig({
63+
...config,
64+
onSettled: (data, error) => {
65+
instance.unsubscribe(true)
66+
config.onSettled?.(data, error)
67+
},
68+
})
69+
5870
throw query.fetch()
5971
}
6072
}

0 commit comments

Comments
 (0)