Skip to content

Commit d6cb962

Browse files
mattpocockTkDodo
andauthored
feat(types): better type narrowing for useQuery when initialData is supplied (#3834)
* Added a proof of concept for initialData * Fleshed out test * Completed overload change * More progress * Testing if a local tsconfig.json quiets down the errors * Fixed TS errors * fix(types): extract DefinedQueryResult and use that in tests * Apply suggestions from code review Co-authored-by: Dominik Dorfmeister <[email protected]>
1 parent 3a02dbb commit d6cb962

File tree

6 files changed

+233
-22
lines changed

6 files changed

+233
-22
lines changed

package-lock.json

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/react-query-persist-client/src/__tests__/PersistQueryClientProvider.test.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
useQuery,
77
UseQueryResult,
88
useQueries,
9+
DefinedUseQueryResult,
910
} from '@tanstack/react-query'
1011
import {
1112
createQueryClient,
@@ -200,7 +201,7 @@ describe('PersistQueryClientProvider', () => {
200201

201202
test('should show initialData while restoring', async () => {
202203
const key = queryKey()
203-
const states: UseQueryResult<string>[] = []
204+
const states: DefinedUseQueryResult<string>[] = []
204205

205206
const queryClient = createQueryClient()
206207
await queryClient.prefetchQuery(key, () => Promise.resolve('hydrated'))

packages/react-query/src/__tests__/useQuery.test.tsx

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
QueryFunction,
2020
QueryFunctionContext,
2121
UseQueryOptions,
22+
DefinedUseQueryResult,
2223
} from '..'
2324
import { ErrorBoundary } from 'react-error-boundary'
2425

@@ -142,7 +143,7 @@ describe('useQuery', () => {
142143
) => Promise<TQueryFnData>,
143144
options?: Omit<
144145
UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
145-
'queryKey' | 'queryFn'
146+
'queryKey' | 'queryFn' | 'initialData'
146147
>,
147148
) => useQuery(qk, () => fetcher(qk[1], 'token'), options)
148149
const test = useWrappedQuery([''], async () => '1')
@@ -159,7 +160,7 @@ describe('useQuery', () => {
159160
fetcher: () => Promise<TQueryFnData>,
160161
options?: Omit<
161162
UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
162-
'queryKey' | 'queryFn'
163+
'queryKey' | 'queryFn' | 'initialData'
163164
>,
164165
) => useQuery(qk, fetcher, options)
165166
const testFuncStyle = useWrappedFuncStyleQuery([''], async () => true)
@@ -1295,7 +1296,7 @@ describe('useQuery', () => {
12951296

12961297
it('should use query function from hook when the existing query does not have a query function', async () => {
12971298
const key = queryKey()
1298-
const results: UseQueryResult<string>[] = []
1299+
const results: DefinedUseQueryResult<string>[] = []
12991300

13001301
queryClient.setQueryData(key, 'set')
13011302

@@ -1712,7 +1713,7 @@ describe('useQuery', () => {
17121713

17131714
it('should not show initial data from next query if keepPreviousData is set', async () => {
17141715
const key = queryKey()
1715-
const states: UseQueryResult<number>[] = []
1716+
const states: DefinedUseQueryResult<number>[] = []
17161717

17171718
function Page() {
17181719
const [count, setCount] = React.useState(0)
@@ -3025,7 +3026,7 @@ describe('useQuery', () => {
30253026

30263027
it('should fetch if initial data is set', async () => {
30273028
const key = queryKey()
3028-
const states: UseQueryResult<string>[] = []
3029+
const states: DefinedUseQueryResult<string>[] = []
30293030

30303031
function Page() {
30313032
const state = useQuery(key, () => 'data', {
@@ -3055,7 +3056,7 @@ describe('useQuery', () => {
30553056

30563057
it('should not fetch if initial data is set with a stale time', async () => {
30573058
const key = queryKey()
3058-
const states: UseQueryResult<string>[] = []
3059+
const states: DefinedUseQueryResult<string>[] = []
30593060

30603061
function Page() {
30613062
const state = useQuery(key, () => 'data', {
@@ -3085,7 +3086,7 @@ describe('useQuery', () => {
30853086

30863087
it('should fetch if initial data updated at is older than stale time', async () => {
30873088
const key = queryKey()
3088-
const states: UseQueryResult<string>[] = []
3089+
const states: DefinedUseQueryResult<string>[] = []
30893090

30903091
const oneSecondAgo = Date.now() - 1000
30913092

@@ -3123,7 +3124,7 @@ describe('useQuery', () => {
31233124

31243125
it('should fetch if "initial data updated at" is exactly 0', async () => {
31253126
const key = queryKey()
3126-
const states: UseQueryResult<string>[] = []
3127+
const states: DefinedUseQueryResult<string>[] = []
31273128

31283129
function Page() {
31293130
const state = useQuery(key, () => 'data', {
@@ -3154,7 +3155,7 @@ describe('useQuery', () => {
31543155

31553156
it('should keep initial data when the query key changes', async () => {
31563157
const key = queryKey()
3157-
const states: UseQueryResult<{ count: number }>[] = []
3158+
const states: DefinedUseQueryResult<{ count: number }>[] = []
31583159

31593160
function Page() {
31603161
const [count, setCount] = React.useState(0)
@@ -3629,7 +3630,7 @@ describe('useQuery', () => {
36293630

36303631
it('should mark query as fetching, when using initialData', async () => {
36313632
const key = queryKey()
3632-
const results: UseQueryResult<string>[] = []
3633+
const results: DefinedUseQueryResult<string>[] = []
36333634

36343635
function Page() {
36353636
const result = useQuery(key, () => 'serverData', { initialData: 'data' })
@@ -3648,7 +3649,7 @@ describe('useQuery', () => {
36483649

36493650
it('should initialize state properly, when initialData is falsy', async () => {
36503651
const key = queryKey()
3651-
const results: UseQueryResult<number>[] = []
3652+
const results: DefinedUseQueryResult<number>[] = []
36523653

36533654
function Page() {
36543655
const result = useQuery(key, () => 1, { initialData: 0 })
@@ -3668,7 +3669,7 @@ describe('useQuery', () => {
36683669
// // See https://github.com/tannerlinsley/react-query/issues/214
36693670
it('data should persist when enabled is changed to false', async () => {
36703671
const key = queryKey()
3671-
const results: UseQueryResult<string>[] = []
3672+
const results: DefinedUseQueryResult<string>[] = []
36723673

36733674
function Page() {
36743675
const [shouldFetch, setShouldFetch] = React.useState(true)
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import { useQuery } from '../useQuery'
2+
3+
export type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends <
4+
T,
5+
>() => T extends Y ? 1 : 2
6+
? true
7+
: false
8+
9+
export type Expect<T extends true> = T
10+
11+
const doNotExecute = (_func: () => void) => true
12+
13+
describe('initialData', () => {
14+
describe('Config object overload', () => {
15+
it('TData should always be defined when initialData is provided as an object', () => {
16+
doNotExecute(() => {
17+
const { data } = useQuery({
18+
queryFn: () => {
19+
return {
20+
wow: true,
21+
}
22+
},
23+
initialData: {
24+
wow: true,
25+
},
26+
})
27+
28+
const result: Expect<Equal<{ wow: boolean }, typeof data>> = true
29+
return result
30+
})
31+
})
32+
33+
it('TData should always be defined when initialData is provided as a function which ALWAYS returns the data', () => {
34+
doNotExecute(() => {
35+
const { data } = useQuery({
36+
queryFn: () => {
37+
return {
38+
wow: true,
39+
}
40+
},
41+
initialData: () => ({
42+
wow: true,
43+
}),
44+
})
45+
46+
const result: Expect<Equal<{ wow: boolean }, typeof data>> = true
47+
return result
48+
})
49+
})
50+
51+
it('TData should have undefined in the union when initialData is NOT provided', () => {
52+
doNotExecute(() => {
53+
const { data } = useQuery({
54+
queryFn: () => {
55+
return {
56+
wow: true,
57+
}
58+
},
59+
})
60+
61+
const result: Expect<Equal<{ wow: boolean } | undefined, typeof data>> =
62+
true
63+
return result
64+
})
65+
})
66+
67+
it('TData should have undefined in the union when initialData is provided as a function which can return undefined', () => {
68+
doNotExecute(() => {
69+
const { data } = useQuery({
70+
queryFn: () => {
71+
return {
72+
wow: true,
73+
}
74+
},
75+
initialData: () => undefined as { wow: boolean } | undefined,
76+
})
77+
78+
const result: Expect<Equal<{ wow: boolean } | undefined, typeof data>> =
79+
true
80+
return result
81+
})
82+
})
83+
})
84+
85+
describe('Query key overload', () => {
86+
it('TData should always be defined when initialData is provided', () => {
87+
doNotExecute(() => {
88+
const { data } = useQuery(['key'], {
89+
queryFn: () => {
90+
return {
91+
wow: true,
92+
}
93+
},
94+
initialData: {
95+
wow: true,
96+
},
97+
})
98+
99+
const result: Expect<Equal<{ wow: boolean }, typeof data>> = true
100+
return result
101+
})
102+
})
103+
104+
it('TData should have undefined in the union when initialData is NOT provided', () => {
105+
doNotExecute(() => {
106+
const { data } = useQuery(['key'], {
107+
queryFn: () => {
108+
return {
109+
wow: true,
110+
}
111+
},
112+
})
113+
114+
const result: Expect<Equal<{ wow: boolean } | undefined, typeof data>> =
115+
true
116+
return result
117+
})
118+
})
119+
})
120+
121+
describe('Query key and func', () => {
122+
it('TData should always be defined when initialData is provided', () => {
123+
doNotExecute(() => {
124+
const { data } = useQuery(
125+
['key'],
126+
() => {
127+
return {
128+
wow: true,
129+
}
130+
},
131+
{
132+
initialData: {
133+
wow: true,
134+
},
135+
},
136+
)
137+
138+
const result: Expect<Equal<{ wow: boolean }, typeof data>> = true
139+
return result
140+
})
141+
})
142+
143+
it('TData should have undefined in the union when initialData is NOT provided', () => {
144+
doNotExecute(() => {
145+
const { data } = useQuery(['key'], () => {
146+
return {
147+
wow: true,
148+
}
149+
})
150+
151+
const result: Expect<Equal<{ wow: boolean } | undefined, typeof data>> =
152+
true
153+
return result
154+
})
155+
})
156+
})
157+
})

packages/react-query/src/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@ export type UseQueryResult<
6565
TError = unknown,
6666
> = UseBaseQueryResult<TData, TError>
6767

68+
export type DefinedUseQueryResult<TData = unknown, TError = unknown> = Omit<
69+
UseQueryResult<TData, TError>,
70+
'data'
71+
> & { data: TData }
72+
6873
export type UseInfiniteQueryResult<
6974
TData = unknown,
7075
TError = unknown,

0 commit comments

Comments
 (0)