Skip to content

Commit 248975e

Browse files
authored
fix(streamedQuery): maintain error state on reset refetch with initialData defined (TanStack#10287)
* fix(streamedQuery): maintain error state on reset refetch with initialData defined * ref: changeset * fix: isRefetch computation * test: add refetch behavior for reset mode after initial error * test: add reset behavior for refetch after initial error with initialData * ref: re-use isFetched in queryObserver.ts * ref: function instead of getter
1 parent a89aab9 commit 248975e

File tree

5 files changed

+198
-10
lines changed

5 files changed

+198
-10
lines changed

.changeset/shaggy-ducks-cheer.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
'@tanstack/query-broadcast-client-experimental': patch
3+
'@tanstack/query-async-storage-persister': patch
4+
'@tanstack/angular-query-persist-client': patch
5+
'@tanstack/query-sync-storage-persister': patch
6+
'@tanstack/preact-query-persist-client': patch
7+
'@tanstack/svelte-query-persist-client': patch
8+
'@tanstack/angular-query-experimental': patch
9+
'@tanstack/react-query-persist-client': patch
10+
'@tanstack/solid-query-persist-client': patch
11+
'@tanstack/query-persist-client-core': patch
12+
'@tanstack/preact-query': patch
13+
'@tanstack/svelte-query': patch
14+
'@tanstack/react-query': patch
15+
'@tanstack/solid-query': patch
16+
'@tanstack/query-core': patch
17+
'@tanstack/vue-query': patch
18+
---
19+
20+
fix(streamedQuery): maintain error state on reset refetch with initialData defined

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

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,171 @@ describe('streamedQuery', () => {
539539
unsubscribe()
540540
})
541541

542+
test('should keep error state on reset refetch when initialData is defined', async () => {
543+
const key = queryKey()
544+
let shouldError = false
545+
const error = new Error('stream failed')
546+
547+
const observer = new QueryObserver(queryClient, {
548+
queryKey: key,
549+
initialData: ['initial'],
550+
retry: false,
551+
queryFn: streamedQuery({
552+
refetchMode: 'reset',
553+
streamFn: async function* () {
554+
if (shouldError) {
555+
throw error
556+
}
557+
558+
yield 0
559+
},
560+
}),
561+
})
562+
563+
const unsubscribe = observer.subscribe(vi.fn())
564+
565+
await vi.advanceTimersByTimeAsync(0)
566+
567+
expect(observer.getCurrentResult()).toMatchObject({
568+
status: 'success',
569+
fetchStatus: 'idle',
570+
data: ['initial', 0],
571+
})
572+
573+
shouldError = true
574+
575+
const refetchPromise = observer.refetch()
576+
577+
await vi.advanceTimersByTimeAsync(0)
578+
await expect(refetchPromise).resolves.toMatchObject({
579+
status: 'error',
580+
error,
581+
data: ['initial'],
582+
})
583+
584+
expect(observer.getCurrentResult()).toMatchObject({
585+
status: 'error',
586+
fetchStatus: 'idle',
587+
data: ['initial'],
588+
error,
589+
})
590+
591+
unsubscribe()
592+
})
593+
594+
test('should treat a fetch after an initial error as a refetch for reset mode', async () => {
595+
const key = queryKey()
596+
let shouldError = true
597+
const error = new Error('stream failed')
598+
599+
const observer = new QueryObserver(queryClient, {
600+
queryKey: key,
601+
retry: false,
602+
queryFn: streamedQuery({
603+
refetchMode: 'reset',
604+
streamFn: async function* () {
605+
if (shouldError) {
606+
throw error
607+
}
608+
609+
yield* createAsyncNumberGenerator(1)
610+
},
611+
}),
612+
})
613+
614+
const unsubscribe = observer.subscribe(vi.fn())
615+
616+
await vi.advanceTimersByTimeAsync(0)
617+
618+
expect(observer.getCurrentResult()).toMatchObject({
619+
status: 'error',
620+
fetchStatus: 'idle',
621+
data: undefined,
622+
error,
623+
})
624+
625+
shouldError = false
626+
627+
void observer.refetch()
628+
629+
await vi.advanceTimersByTimeAsync(0)
630+
631+
expect(observer.getCurrentResult()).toMatchObject({
632+
status: 'pending',
633+
fetchStatus: 'fetching',
634+
data: undefined,
635+
error: null,
636+
})
637+
638+
await vi.advanceTimersByTimeAsync(50)
639+
640+
expect(observer.getCurrentResult()).toMatchObject({
641+
status: 'success',
642+
fetchStatus: 'idle',
643+
data: [0],
644+
error: null,
645+
})
646+
647+
unsubscribe()
648+
})
649+
650+
test('should reset to initialData on refetch after an initial error', async () => {
651+
const key = queryKey()
652+
let shouldError = true
653+
const error = new Error('stream failed')
654+
655+
const observer = new QueryObserver(queryClient, {
656+
queryKey: key,
657+
initialData: ['initial'],
658+
retry: false,
659+
queryFn: streamedQuery({
660+
refetchMode: 'reset',
661+
streamFn: async function* () {
662+
if (shouldError) {
663+
throw error
664+
}
665+
666+
yield* createAsyncNumberGenerator(1)
667+
},
668+
}),
669+
})
670+
671+
const unsubscribe = observer.subscribe(vi.fn())
672+
673+
await vi.advanceTimersByTimeAsync(0)
674+
675+
expect(observer.getCurrentResult()).toMatchObject({
676+
status: 'error',
677+
fetchStatus: 'idle',
678+
data: ['initial'],
679+
error,
680+
})
681+
682+
shouldError = false
683+
684+
void observer.refetch()
685+
686+
await vi.advanceTimersByTimeAsync(0)
687+
688+
expect(observer.getCurrentResult()).toMatchObject({
689+
status: 'success',
690+
fetchStatus: 'fetching',
691+
data: ['initial'],
692+
error: null,
693+
})
694+
695+
await vi.advanceTimersByTimeAsync(50)
696+
697+
expect(observer.getCurrentResult()).toMatchObject({
698+
status: 'success',
699+
fetchStatus: 'idle',
700+
data: ['initial', 0],
701+
error: null,
702+
})
703+
704+
unsubscribe()
705+
})
706+
542707
test('should not call reducer twice when refetchMode is replace', async () => {
543708
const key = queryKey()
544709
const arr: Array<number> = []

packages/query-core/src/query.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -260,9 +260,13 @@ export class Query<
260260
this.cancel({ silent: true })
261261
}
262262

263+
get resetState(): QueryState<TData, TError> {
264+
return this.#initialState
265+
}
266+
263267
reset(): void {
264268
this.destroy()
265-
this.setState(this.#initialState)
269+
this.setState(this.resetState)
266270
}
267271

268272
isActive(): boolean {
@@ -276,10 +280,11 @@ export class Query<
276280
return !this.isActive()
277281
}
278282
// if a query has no observers, it should still be considered disabled if it never attempted a fetch
279-
return (
280-
this.options.queryFn === skipToken ||
281-
this.state.dataUpdateCount + this.state.errorUpdateCount === 0
282-
)
283+
return this.options.queryFn === skipToken || !this.isFetched()
284+
}
285+
286+
isFetched() {
287+
return this.state.dataUpdateCount + this.state.errorUpdateCount > 0
283288
}
284289

285290
isStatic(): boolean {

packages/query-core/src/queryObserver.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -576,7 +576,7 @@ export class QueryObserver<
576576
failureCount: newState.fetchFailureCount,
577577
failureReason: newState.fetchFailureReason,
578578
errorUpdateCount: newState.errorUpdateCount,
579-
isFetched: newState.dataUpdateCount > 0 || newState.errorUpdateCount > 0,
579+
isFetched: query.isFetched(),
580580
isFetchedAfterMount:
581581
newState.dataUpdateCount > queryInitialState.dataUpdateCount ||
582582
newState.errorUpdateCount > queryInitialState.errorUpdateCount,

packages/query-core/src/streamedQuery.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,10 @@ export function streamedQuery<
6666
const query = context.client
6767
.getQueryCache()
6868
.find({ queryKey: context.queryKey, exact: true })
69-
const isRefetch = !!query && query.state.data !== undefined
69+
const isRefetch = !!query && query.isFetched()
7070
if (isRefetch && refetchMode === 'reset') {
7171
query.setState({
72-
status: 'pending',
73-
data: undefined,
74-
error: null,
72+
...query.resetState,
7573
fetchStatus: 'fetching',
7674
})
7775
}

0 commit comments

Comments
 (0)