Skip to content

Commit 7d85074

Browse files
authored
feat: add discriminated unions for query results (#1247)
1 parent 3e0a874 commit 7d85074

File tree

5 files changed

+229
-26
lines changed

5 files changed

+229
-26
lines changed

src/core/queryObserver.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
import { notifyManager } from './notifyManager'
1111
import type {
1212
PlaceholderDataFunction,
13+
QueryObserverBaseResult,
1314
QueryObserverOptions,
1415
QueryObserverResult,
1516
QueryOptions,
@@ -390,21 +391,25 @@ export class QueryObserver<
390391
}
391392
}
392393

393-
return {
394+
const result: QueryObserverBaseResult<TData, TError> = {
394395
...getStatusProps(status),
395396
data,
396397
error: state.error,
397398
failureCount: state.fetchFailureCount,
398399
isFetched: state.dataUpdateCount > 0,
399400
isFetchedAfterMount: state.dataUpdateCount > this.initialDataUpdateCount,
400401
isFetching,
402+
isLoadingError: status === 'error' && state.dataUpdatedAt === 0,
401403
isPlaceholderData,
402404
isPreviousData,
405+
isRefetchError: status === 'error' && state.dataUpdatedAt !== 0,
403406
isStale: this.isStale(),
404407
refetch: this.refetch,
405408
remove: this.remove,
406409
updatedAt,
407410
}
411+
412+
return result as QueryObserverResult<TData, TError>
408413
}
409414

410415
private updateResult(willFetch?: boolean): void {

src/core/types.ts

Lines changed: 166 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ export interface FetchPreviousPageOptions extends ResultOptions {
217217

218218
export type QueryStatus = 'idle' | 'loading' | 'error' | 'success'
219219

220-
export interface QueryObserverResult<TData = unknown, TError = unknown> {
220+
export interface QueryObserverBaseResult<TData = unknown, TError = unknown> {
221221
data: TData | undefined
222222
error: TError | null
223223
failureCount: number
@@ -227,8 +227,10 @@ export interface QueryObserverResult<TData = unknown, TError = unknown> {
227227
isFetching: boolean
228228
isIdle: boolean
229229
isLoading: boolean
230-
isPreviousData: boolean
230+
isLoadingError: boolean
231231
isPlaceholderData: boolean
232+
isPreviousData: boolean
233+
isRefetchError: boolean
232234
isStale: boolean
233235
isSuccess: boolean
234236
refetch: (
@@ -239,8 +241,86 @@ export interface QueryObserverResult<TData = unknown, TError = unknown> {
239241
updatedAt: number
240242
}
241243

242-
export interface InfiniteQueryObserverResult<TData = unknown, TError = unknown>
243-
extends QueryObserverResult<InfiniteData<TData>, TError> {
244+
export interface QueryObserverIdleResult<TData = unknown, TError = unknown>
245+
extends QueryObserverBaseResult<TData, TError> {
246+
data: undefined
247+
error: null
248+
isError: false
249+
isIdle: true
250+
isLoading: false
251+
isLoadingError: false
252+
isRefetchError: false
253+
isSuccess: false
254+
status: 'idle'
255+
}
256+
257+
export interface QueryObserverLoadingResult<TData = unknown, TError = unknown>
258+
extends QueryObserverBaseResult<TData, TError> {
259+
data: undefined
260+
error: null
261+
isError: false
262+
isIdle: false
263+
isLoading: true
264+
isLoadingError: false
265+
isRefetchError: false
266+
isSuccess: false
267+
status: 'loading'
268+
}
269+
270+
export interface QueryObserverLoadingErrorResult<
271+
TData = unknown,
272+
TError = unknown
273+
> extends QueryObserverBaseResult<TData, TError> {
274+
data: undefined
275+
error: TError
276+
isError: true
277+
isIdle: false
278+
isLoading: false
279+
isLoadingError: true
280+
isRefetchError: false
281+
isSuccess: false
282+
status: 'error'
283+
}
284+
285+
export interface QueryObserverRefetchErrorResult<
286+
TData = unknown,
287+
TError = unknown
288+
> extends QueryObserverBaseResult<TData, TError> {
289+
data: TData
290+
error: TError
291+
isError: true
292+
isIdle: false
293+
isLoading: false
294+
isLoadingError: false
295+
isRefetchError: true
296+
isSuccess: false
297+
status: 'error'
298+
}
299+
300+
export interface QueryObserverSuccessResult<TData = unknown, TError = unknown>
301+
extends QueryObserverBaseResult<TData, TError> {
302+
data: TData
303+
error: null
304+
isError: false
305+
isIdle: false
306+
isLoading: false
307+
isLoadingError: false
308+
isRefetchError: false
309+
isSuccess: true
310+
status: 'success'
311+
}
312+
313+
export type QueryObserverResult<TData = unknown, TError = unknown> =
314+
| QueryObserverIdleResult<TData, TError>
315+
| QueryObserverLoadingErrorResult<TData, TError>
316+
| QueryObserverLoadingResult<TData, TError>
317+
| QueryObserverRefetchErrorResult<TData, TError>
318+
| QueryObserverSuccessResult<TData, TError>
319+
320+
export interface InfiniteQueryObserverBaseResult<
321+
TData = unknown,
322+
TError = unknown
323+
> extends QueryObserverBaseResult<InfiniteData<TData>, TError> {
244324
fetchNextPage: (
245325
options?: FetchNextPageOptions
246326
) => Promise<InfiniteQueryObserverResult<TData, TError>>
@@ -253,6 +333,88 @@ export interface InfiniteQueryObserverResult<TData = unknown, TError = unknown>
253333
isFetchingPreviousPage: boolean
254334
}
255335

336+
export interface InfiniteQueryObserverIdleResult<
337+
TData = unknown,
338+
TError = unknown
339+
> extends InfiniteQueryObserverBaseResult<TData, TError> {
340+
data: undefined
341+
error: null
342+
isError: false
343+
isIdle: true
344+
isLoading: false
345+
isLoadingError: false
346+
isRefetchError: false
347+
isSuccess: false
348+
status: 'idle'
349+
}
350+
351+
export interface InfiniteQueryObserverLoadingResult<
352+
TData = unknown,
353+
TError = unknown
354+
> extends InfiniteQueryObserverBaseResult<TData, TError> {
355+
data: undefined
356+
error: null
357+
isError: false
358+
isIdle: false
359+
isLoading: true
360+
isLoadingError: false
361+
isRefetchError: false
362+
isSuccess: false
363+
status: 'loading'
364+
}
365+
366+
export interface InfiniteQueryObserverLoadingErrorResult<
367+
TData = unknown,
368+
TError = unknown
369+
> extends InfiniteQueryObserverBaseResult<TData, TError> {
370+
data: undefined
371+
error: TError
372+
isError: true
373+
isIdle: false
374+
isLoading: false
375+
isLoadingError: true
376+
isRefetchError: false
377+
isSuccess: false
378+
status: 'error'
379+
}
380+
381+
export interface InfiniteQueryObserverRefetchErrorResult<
382+
TData = unknown,
383+
TError = unknown
384+
> extends InfiniteQueryObserverBaseResult<TData, TError> {
385+
data: InfiniteData<TData>
386+
error: TError
387+
isError: true
388+
isIdle: false
389+
isLoading: false
390+
isLoadingError: false
391+
isRefetchError: true
392+
isSuccess: false
393+
status: 'error'
394+
}
395+
396+
export interface InfiniteQueryObserverSuccessResult<
397+
TData = unknown,
398+
TError = unknown
399+
> extends InfiniteQueryObserverBaseResult<TData, TError> {
400+
data: InfiniteData<TData>
401+
error: null
402+
isError: false
403+
isIdle: false
404+
isLoading: false
405+
isLoadingError: false
406+
isRefetchError: false
407+
isSuccess: true
408+
status: 'success'
409+
}
410+
411+
export type InfiniteQueryObserverResult<TData = unknown, TError = unknown> =
412+
| InfiniteQueryObserverIdleResult<TData, TError>
413+
| InfiniteQueryObserverLoadingErrorResult<TData, TError>
414+
| InfiniteQueryObserverLoadingResult<TData, TError>
415+
| InfiniteQueryObserverRefetchErrorResult<TData, TError>
416+
| InfiniteQueryObserverSuccessResult<TData, TError>
417+
256418
export type MutationKey = string | unknown[]
257419

258420
export type MutationStatus = 'idle' | 'loading' | 'success' | 'error'

src/react/tests/useInfiniteQuery.test.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,10 @@ describe('useInfiniteQuery', () => {
8787
isFetchingPreviousPage: false,
8888
isIdle: false,
8989
isLoading: true,
90-
isPreviousData: false,
90+
isLoadingError: false,
9191
isPlaceholderData: false,
92+
isPreviousData: false,
93+
isRefetchError: false,
9294
isStale: true,
9395
isSuccess: false,
9496
refetch: expect.any(Function),
@@ -113,8 +115,10 @@ describe('useInfiniteQuery', () => {
113115
isFetchingPreviousPage: false,
114116
isIdle: false,
115117
isLoading: false,
116-
isPreviousData: false,
118+
isLoadingError: false,
117119
isPlaceholderData: false,
120+
isPreviousData: false,
121+
isRefetchError: false,
118122
isStale: true,
119123
isSuccess: true,
120124
refetch: expect.any(Function),

src/react/tests/useQuery.test.tsx

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -116,20 +116,36 @@ describe('useQuery', () => {
116116
const states: UseQueryResult<string>[] = []
117117

118118
function Page() {
119-
const state = useQuery(key, () => 'test')
119+
const state = useQuery<string, Error>(key, () => 'test')
120120

121121
states.push(state)
122122

123-
return (
124-
<div>
125-
<h1>Status: {state.status}</h1>
126-
</div>
127-
)
123+
if (state.isIdle) {
124+
expectType<undefined>(state.data)
125+
expectType<null>(state.error)
126+
return <span>idle</span>
127+
}
128+
129+
if (state.isLoading) {
130+
expectType<undefined>(state.data)
131+
expectType<null>(state.error)
132+
return <span>loading</span>
133+
}
134+
135+
if (state.isLoadingError) {
136+
expectType<undefined>(state.data)
137+
expectType<Error>(state.error)
138+
return <span>{state.error}</span>
139+
}
140+
141+
expectType<string>(state.data)
142+
expectType<Error | null>(state.error)
143+
return <span>{state.data}</span>
128144
}
129145

130-
const rendered = renderWithClient(queryClient, <Page />)
146+
renderWithClient(queryClient, <Page />)
131147

132-
await waitFor(() => rendered.getByText('Status: success'))
148+
await sleep(10)
133149

134150
expect(states[0]).toEqual({
135151
data: undefined,
@@ -141,8 +157,10 @@ describe('useQuery', () => {
141157
isFetching: true,
142158
isIdle: false,
143159
isLoading: true,
144-
isPreviousData: false,
160+
isLoadingError: false,
145161
isPlaceholderData: false,
162+
isPreviousData: false,
163+
isRefetchError: false,
146164
isStale: true,
147165
isSuccess: false,
148166
refetch: expect.any(Function),
@@ -161,8 +179,10 @@ describe('useQuery', () => {
161179
isFetching: false,
162180
isIdle: false,
163181
isLoading: false,
164-
isPreviousData: false,
182+
isLoadingError: false,
165183
isPlaceholderData: false,
184+
isPreviousData: false,
185+
isRefetchError: false,
166186
isStale: true,
167187
isSuccess: true,
168188
refetch: expect.any(Function),
@@ -211,8 +231,10 @@ describe('useQuery', () => {
211231
isFetching: true,
212232
isIdle: false,
213233
isLoading: true,
214-
isPreviousData: false,
234+
isLoadingError: false,
215235
isPlaceholderData: false,
236+
isPreviousData: false,
237+
isRefetchError: false,
216238
isStale: true,
217239
isSuccess: false,
218240
refetch: expect.any(Function),
@@ -231,8 +253,10 @@ describe('useQuery', () => {
231253
isFetching: true,
232254
isIdle: false,
233255
isLoading: true,
234-
isPreviousData: false,
256+
isLoadingError: false,
235257
isPlaceholderData: false,
258+
isPreviousData: false,
259+
isRefetchError: false,
236260
isStale: true,
237261
isSuccess: false,
238262
refetch: expect.any(Function),
@@ -251,8 +275,10 @@ describe('useQuery', () => {
251275
isFetching: false,
252276
isIdle: false,
253277
isLoading: false,
254-
isPreviousData: false,
278+
isLoadingError: true,
255279
isPlaceholderData: false,
280+
isPreviousData: false,
281+
isRefetchError: false,
256282
isStale: true,
257283
isSuccess: false,
258284
refetch: expect.any(Function),

src/react/types.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,20 @@ export interface UseInfiniteQueryOptions<
3434
TQueryData
3535
> {}
3636

37-
export interface UseBaseQueryResult<TData = unknown, TError = unknown>
38-
extends QueryObserverResult<TData, TError> {}
37+
export type UseBaseQueryResult<
38+
TData = unknown,
39+
TError = unknown
40+
> = QueryObserverResult<TData, TError>
3941

40-
export interface UseQueryResult<TData = unknown, TError = unknown>
41-
extends UseBaseQueryResult<TData, TError> {}
42+
export type UseQueryResult<
43+
TData = unknown,
44+
TError = unknown
45+
> = UseBaseQueryResult<TData, TError>
4246

43-
export interface UseInfiniteQueryResult<TData = unknown, TError = unknown>
44-
extends InfiniteQueryObserverResult<TData, TError> {}
47+
export type UseInfiniteQueryResult<
48+
TData = unknown,
49+
TError = unknown
50+
> = InfiniteQueryObserverResult<TData, TError>
4551

4652
export interface UseMutationOptions<
4753
TData = unknown,

0 commit comments

Comments
 (0)