Skip to content

Commit 43ef2a8

Browse files
committed
fix(query-core): prevent state override when observer remount occurs with signal consumption
1 parent 7c464e3 commit 43ef2a8

File tree

4 files changed

+57
-4
lines changed

4 files changed

+57
-4
lines changed

packages/query-core/src/__tests__/query.test.tsx

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1192,4 +1192,48 @@ describe('query', () => {
11921192
expect(initialDataFn).toHaveBeenCalledTimes(1)
11931193
expect(query.state.data).toBe('initial data')
11941194
})
1195+
1196+
test('should not override fetching state when revert happens after new observer subscribes', async () => {
1197+
const key = queryKey()
1198+
1199+
// @ts-expect-error This field has been added for troubleshooting purposes. Disable ts error for testing.
1200+
const queryFn = vi.fn(async ({ signal }) => {
1201+
await sleep(50)
1202+
return 'data'
1203+
})
1204+
1205+
const query = new Query({
1206+
client: queryClient,
1207+
queryKey: key,
1208+
queryHash: hashQueryKeyByOptions(key),
1209+
options: { queryFn },
1210+
})
1211+
1212+
const observer1 = new QueryObserver(queryClient, {
1213+
queryKey: key,
1214+
queryFn,
1215+
})
1216+
1217+
query.addObserver(observer1)
1218+
const promise1 = query.fetch()
1219+
1220+
await vi.advanceTimersByTimeAsync(10)
1221+
1222+
query.removeObserver(observer1)
1223+
1224+
const observer2 = new QueryObserver(queryClient, {
1225+
queryKey: key,
1226+
queryFn,
1227+
})
1228+
1229+
query.addObserver(observer2)
1230+
1231+
query.fetch()
1232+
1233+
await promise1.catch(() => {})
1234+
1235+
await Promise.resolve()
1236+
1237+
expect(query.state.fetchStatus).toBe('fetching')
1238+
})
11951239
})

packages/query-core/src/query.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,7 @@ export class Query<
349349
// we'll let the query continue so the result can be cached
350350
if (this.#retryer) {
351351
if (this.#abortSignalConsumed) {
352-
this.#retryer.cancel({ revert: true })
352+
this.#retryer.cancel({ revert: true, isObserverRemoval: true })
353353
} else {
354354
this.#retryer.cancelRetry()
355355
}
@@ -553,16 +553,22 @@ export class Query<
553553
// so we hatch onto that promise
554554
return this.#retryer.promise
555555
} else if (error.revert) {
556+
if (error.isObserverRemoval && this.observers.length > 0) {
557+
if (this.state.data === undefined) {
558+
throw error
559+
}
560+
return this.state.data
561+
}
562+
556563
this.setState({
557564
...this.#revertState,
558565
fetchStatus: 'idle' as const,
559566
})
560-
// transform error into reverted state data
561-
// if the initial fetch was cancelled, we have no data, so we have
562-
// to get reject with a CancelledError
567+
563568
if (this.state.data === undefined) {
564569
throw error
565570
}
571+
566572
return this.state.data
567573
}
568574
}

packages/query-core/src/retryer.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,12 @@ export function canFetch(networkMode: NetworkMode | undefined): boolean {
5858
export class CancelledError extends Error {
5959
revert?: boolean
6060
silent?: boolean
61+
isObserverRemoval?: boolean
6162
constructor(options?: CancelOptions) {
6263
super('CancelledError')
6364
this.revert = options?.revert
6465
this.silent = options?.silent
66+
this.isObserverRemoval = options?.isObserverRemoval
6567
}
6668
}
6769

packages/query-core/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1329,6 +1329,7 @@ export interface DefaultOptions<TError = DefaultError> {
13291329
export interface CancelOptions {
13301330
revert?: boolean
13311331
silent?: boolean
1332+
isObserverRemoval?: boolean
13321333
}
13331334

13341335
export interface SetDataOptions {

0 commit comments

Comments
 (0)