@tanstack/react-query 5.8.3 → 5.8.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/build/codemods/coverage/clover.xml +2 -2
  2. package/build/codemods/coverage/index.html +1 -1
  3. package/build/legacy/suspense.cjs +1 -1
  4. package/build/legacy/suspense.cjs.map +1 -1
  5. package/build/legacy/suspense.d.cts +1 -1
  6. package/build/legacy/suspense.d.ts +1 -1
  7. package/build/legacy/suspense.js +1 -1
  8. package/build/legacy/suspense.js.map +1 -1
  9. package/build/legacy/useBaseQuery.cjs +2 -1
  10. package/build/legacy/useBaseQuery.cjs.map +1 -1
  11. package/build/legacy/useBaseQuery.js +2 -1
  12. package/build/legacy/useBaseQuery.js.map +1 -1
  13. package/build/legacy/useQueries.cjs +9 -2
  14. package/build/legacy/useQueries.cjs.map +1 -1
  15. package/build/legacy/useQueries.d.cts +18 -2
  16. package/build/legacy/useQueries.d.ts +18 -2
  17. package/build/legacy/useQueries.js +9 -2
  18. package/build/legacy/useQueries.js.map +1 -1
  19. package/build/legacy/useSuspenseQueries.cjs.map +1 -1
  20. package/build/legacy/useSuspenseQueries.js.map +1 -1
  21. package/build/modern/suspense.cjs +1 -1
  22. package/build/modern/suspense.cjs.map +1 -1
  23. package/build/modern/suspense.d.cts +1 -1
  24. package/build/modern/suspense.d.ts +1 -1
  25. package/build/modern/suspense.js +1 -1
  26. package/build/modern/suspense.js.map +1 -1
  27. package/build/modern/useBaseQuery.cjs +2 -1
  28. package/build/modern/useBaseQuery.cjs.map +1 -1
  29. package/build/modern/useBaseQuery.js +2 -1
  30. package/build/modern/useBaseQuery.js.map +1 -1
  31. package/build/modern/useQueries.cjs +9 -2
  32. package/build/modern/useQueries.cjs.map +1 -1
  33. package/build/modern/useQueries.d.cts +18 -2
  34. package/build/modern/useQueries.d.ts +18 -2
  35. package/build/modern/useQueries.js +9 -2
  36. package/build/modern/useQueries.js.map +1 -1
  37. package/build/modern/useSuspenseQueries.cjs.map +1 -1
  38. package/build/modern/useSuspenseQueries.js.map +1 -1
  39. package/package.json +2 -2
  40. package/src/__tests__/ssr.test.tsx +1 -5
  41. package/src/__tests__/suspense.test.tsx +146 -0
  42. package/src/__tests__/useInfiniteQuery.test.tsx +4 -4
  43. package/src/__tests__/useQuery.test.tsx +0 -9
  44. package/src/suspense.ts +1 -2
  45. package/src/useBaseQuery.ts +5 -1
  46. package/src/useQueries.ts +133 -100
  47. package/src/useSuspenseQueries.ts +119 -104
@@ -887,4 +887,150 @@ describe('useSuspenseQueries', () => {
887
887
  await waitFor(() => rendered.getByText('data: 1,2'))
888
888
  expect(refs[0]).toBe(refs[1])
889
889
  })
890
+
891
+ // this addresses the following issue:
892
+ // https://github.com/TanStack/query/issues/6344
893
+ it('should suspend on offline when query changes, and data should not be undefined', async () => {
894
+ function Page() {
895
+ const [id, setId] = React.useState(0)
896
+
897
+ const { data } = useSuspenseQuery({
898
+ queryKey: [id],
899
+ queryFn: () => Promise.resolve(`Data ${id}`),
900
+ })
901
+
902
+ // defensive guard here
903
+ if (data === undefined) {
904
+ throw new Error('data cannot be undefined')
905
+ }
906
+
907
+ return (
908
+ <>
909
+ <div>{data}</div>
910
+ <button onClick={() => setId(id + 1)}>fetch</button>
911
+ </>
912
+ )
913
+ }
914
+
915
+ const rendered = renderWithClient(
916
+ queryClient,
917
+ <React.Suspense fallback={<div>loading</div>}>
918
+ <Page />
919
+ </React.Suspense>,
920
+ )
921
+
922
+ await waitFor(() => rendered.getByText('loading'))
923
+ await waitFor(() => rendered.getByText('Data 0'))
924
+
925
+ // go offline
926
+ document.dispatchEvent(new CustomEvent('offline'))
927
+
928
+ fireEvent.click(rendered.getByText('fetch'))
929
+ await waitFor(() => rendered.getByText('Data 0'))
930
+
931
+ // go back online
932
+ document.dispatchEvent(new CustomEvent('online'))
933
+ fireEvent.click(rendered.getByText('fetch'))
934
+
935
+ // query should resume
936
+ await waitFor(() => rendered.getByText('Data 1'))
937
+ })
938
+
939
+ it('should throw error when queryKey changes and new query fails', async () => {
940
+ const consoleMock = vi
941
+ .spyOn(console, 'error')
942
+ .mockImplementation(() => undefined)
943
+ const key = queryKey()
944
+
945
+ function Page() {
946
+ const [fail, setFail] = React.useState(false)
947
+ const { data } = useSuspenseQuery({
948
+ queryKey: [key, fail],
949
+ queryFn: async () => {
950
+ await sleep(10)
951
+
952
+ if (fail) {
953
+ throw new Error('Suspense Error Bingo')
954
+ } else {
955
+ return 'data'
956
+ }
957
+ },
958
+ retry: 0,
959
+ })
960
+
961
+ return (
962
+ <div>
963
+ <button onClick={() => setFail(true)}>trigger fail</button>
964
+
965
+ <div>rendered: {String(data)}</div>
966
+ </div>
967
+ )
968
+ }
969
+
970
+ const rendered = renderWithClient(
971
+ queryClient,
972
+ <ErrorBoundary fallbackRender={() => <div>error boundary</div>}>
973
+ <React.Suspense fallback={'Loading...'}>
974
+ <Page />
975
+ </React.Suspense>
976
+ </ErrorBoundary>,
977
+ )
978
+
979
+ await waitFor(() => rendered.getByText('Loading...'))
980
+
981
+ await waitFor(() => rendered.getByText('rendered: data'))
982
+
983
+ fireEvent.click(rendered.getByText('trigger fail'))
984
+
985
+ await waitFor(() => rendered.getByText('error boundary'))
986
+
987
+ expect(consoleMock).toHaveBeenCalledWith(
988
+ expect.objectContaining(new Error('Suspense Error Bingo')),
989
+ )
990
+
991
+ consoleMock.mockRestore()
992
+ })
993
+
994
+ it('should keep previous data when wrapped in a transition', async () => {
995
+ const key = queryKey()
996
+
997
+ function Page() {
998
+ const [count, setCount] = React.useState(0)
999
+ const [isPending, startTransition] = React.useTransition()
1000
+ const { data } = useSuspenseQuery({
1001
+ queryKey: [key, count],
1002
+ queryFn: async () => {
1003
+ await sleep(10)
1004
+ return 'data' + count
1005
+ },
1006
+ })
1007
+
1008
+ return (
1009
+ <div>
1010
+ <button onClick={() => startTransition(() => setCount(count + 1))}>
1011
+ inc
1012
+ </button>
1013
+
1014
+ <div>{isPending ? 'Pending...' : String(data)}</div>
1015
+ </div>
1016
+ )
1017
+ }
1018
+
1019
+ const rendered = renderWithClient(
1020
+ queryClient,
1021
+ <React.Suspense fallback={'Loading...'}>
1022
+ <Page />
1023
+ </React.Suspense>,
1024
+ )
1025
+
1026
+ await waitFor(() => rendered.getByText('Loading...'))
1027
+
1028
+ await waitFor(() => rendered.getByText('data0'))
1029
+
1030
+ fireEvent.click(rendered.getByText('inc'))
1031
+
1032
+ await waitFor(() => rendered.getByText('Pending...'))
1033
+
1034
+ await waitFor(() => rendered.getByText('data1'))
1035
+ })
890
1036
  })
@@ -1253,8 +1253,8 @@ describe('useInfiniteQuery', () => {
1253
1253
  {isFetchingNextPage
1254
1254
  ? 'Loading more...'
1255
1255
  : hasNextPage
1256
- ? 'Load More'
1257
- : 'Nothing more to load'}
1256
+ ? 'Load More'
1257
+ : 'Nothing more to load'}
1258
1258
  </button>
1259
1259
  <button onClick={() => refetch()}>Refetch</button>
1260
1260
  <button
@@ -1382,8 +1382,8 @@ describe('useInfiniteQuery', () => {
1382
1382
  {isFetchingNextPage
1383
1383
  ? 'Loading more...'
1384
1384
  : hasNextPage
1385
- ? 'Load More'
1386
- : 'Nothing more to load'}
1385
+ ? 'Load More'
1386
+ : 'Nothing more to load'}
1387
1387
  </button>
1388
1388
  <button onClick={() => refetch()}>Refetch</button>
1389
1389
  <button onClick={() => setIsRemovedLastPage(true)}>
@@ -1108,7 +1108,6 @@ describe('useQuery', () => {
1108
1108
 
1109
1109
  it('should use query function from hook when the existing query does not have a query function', async () => {
1110
1110
  const key = queryKey()
1111
- const results: Array<DefinedUseQueryResult<string>> = []
1112
1111
 
1113
1112
  queryClient.setQueryData(key, 'set')
1114
1113
 
@@ -1124,8 +1123,6 @@ describe('useQuery', () => {
1124
1123
  staleTime: Infinity,
1125
1124
  })
1126
1125
 
1127
- results.push(result)
1128
-
1129
1126
  return (
1130
1127
  <div>
1131
1128
  <div>isFetching: {result.isFetching}</div>
@@ -1142,12 +1139,6 @@ describe('useQuery', () => {
1142
1139
  await waitFor(() => rendered.getByText('data: set'))
1143
1140
  fireEvent.click(rendered.getByRole('button', { name: /refetch/i }))
1144
1141
  await waitFor(() => rendered.getByText('data: fetched'))
1145
-
1146
- await waitFor(() => expect(results.length).toBe(3))
1147
-
1148
- expect(results[0]).toMatchObject({ data: 'set', isFetching: false })
1149
- expect(results[1]).toMatchObject({ data: 'set', isFetching: true })
1150
- expect(results[2]).toMatchObject({ data: 'fetched', isFetching: false })
1151
1142
  })
1152
1143
 
1153
1144
  it('should update query stale state and refetch when invalidated with invalidateQueries', async () => {
package/src/suspense.ts CHANGED
@@ -40,8 +40,7 @@ export const shouldSuspend = (
40
40
  | DefaultedQueryObserverOptions<any, any, any, any, any>
41
41
  | undefined,
42
42
  result: QueryObserverResult<any, any>,
43
- isRestoring: boolean,
44
- ) => defaultedOptions?.suspense && willFetch(result, isRestoring)
43
+ ) => defaultedOptions?.suspense && result.isPending
45
44
 
46
45
  export const fetchOptimistic = <
47
46
  TQueryFnData,
@@ -90,7 +90,11 @@ export function useBaseQuery<
90
90
  }, [defaultedOptions, observer])
91
91
 
92
92
  // Handle suspense
93
- if (shouldSuspend(defaultedOptions, result, isRestoring)) {
93
+ if (shouldSuspend(defaultedOptions, result)) {
94
+ // Do the same thing as the effect right above because the effect won't run
95
+ // when we suspend but also, the component won't re-mount so our observer would
96
+ // be out of date.
97
+ observer.setOptions(defaultedOptions, { listeners: false })
94
98
  throw fetchOptimistic(defaultedOptions, observer, errorResetBoundary)
95
99
  }
96
100
 
package/src/useQueries.ts CHANGED
@@ -57,68 +57,79 @@ type GetOptions<T> =
57
57
  }
58
58
  ? UseQueryOptionsForUseQueries<TQueryFnData, TError, TData>
59
59
  : T extends { queryFnData: infer TQueryFnData; error?: infer TError }
60
- ? UseQueryOptionsForUseQueries<TQueryFnData, TError>
61
- : T extends { data: infer TData; error?: infer TError }
62
- ? UseQueryOptionsForUseQueries<unknown, TError, TData>
63
- : // Part 2: responsible for applying explicit type parameter to function arguments, if tuple [TQueryFnData, TError, TData]
64
- T extends [infer TQueryFnData, infer TError, infer TData]
65
- ? UseQueryOptionsForUseQueries<TQueryFnData, TError, TData>
66
- : T extends [infer TQueryFnData, infer TError]
67
- ? UseQueryOptionsForUseQueries<TQueryFnData, TError>
68
- : T extends [infer TQueryFnData]
69
- ? UseQueryOptionsForUseQueries<TQueryFnData>
70
- : // Part 3: responsible for inferring and enforcing type if no explicit parameter was provided
71
- T extends {
72
- queryFn?: QueryFunction<infer TQueryFnData, infer TQueryKey>
73
- select: (data: any) => infer TData
74
- throwOnError?: ThrowOnError<any, infer TError, any, any>
75
- }
76
- ? UseQueryOptionsForUseQueries<TQueryFnData, TError, TData, TQueryKey>
77
- : T extends {
78
- queryFn?: QueryFunction<infer TQueryFnData, infer TQueryKey>
79
- throwOnError?: ThrowOnError<any, infer TError, any, any>
80
- }
81
- ? UseQueryOptionsForUseQueries<
82
- TQueryFnData,
83
- TError,
84
- TQueryFnData,
85
- TQueryKey
86
- >
87
- : // Fallback
88
- UseQueryOptionsForUseQueries
60
+ ? UseQueryOptionsForUseQueries<TQueryFnData, TError>
61
+ : T extends { data: infer TData; error?: infer TError }
62
+ ? UseQueryOptionsForUseQueries<unknown, TError, TData>
63
+ : // Part 2: responsible for applying explicit type parameter to function arguments, if tuple [TQueryFnData, TError, TData]
64
+ T extends [infer TQueryFnData, infer TError, infer TData]
65
+ ? UseQueryOptionsForUseQueries<TQueryFnData, TError, TData>
66
+ : T extends [infer TQueryFnData, infer TError]
67
+ ? UseQueryOptionsForUseQueries<TQueryFnData, TError>
68
+ : T extends [infer TQueryFnData]
69
+ ? UseQueryOptionsForUseQueries<TQueryFnData>
70
+ : // Part 3: responsible for inferring and enforcing type if no explicit parameter was provided
71
+ T extends {
72
+ queryFn?: QueryFunction<infer TQueryFnData, infer TQueryKey>
73
+ select: (data: any) => infer TData
74
+ throwOnError?: ThrowOnError<any, infer TError, any, any>
75
+ }
76
+ ? UseQueryOptionsForUseQueries<
77
+ TQueryFnData,
78
+ TError,
79
+ TData,
80
+ TQueryKey
81
+ >
82
+ : T extends {
83
+ queryFn?: QueryFunction<
84
+ infer TQueryFnData,
85
+ infer TQueryKey
86
+ >
87
+ throwOnError?: ThrowOnError<any, infer TError, any, any>
88
+ }
89
+ ? UseQueryOptionsForUseQueries<
90
+ TQueryFnData,
91
+ TError,
92
+ TQueryFnData,
93
+ TQueryKey
94
+ >
95
+ : // Fallback
96
+ UseQueryOptionsForUseQueries
89
97
 
90
98
  type GetResults<T> =
91
99
  // Part 1: responsible for mapping explicit type parameter to function result, if object
92
100
  T extends { queryFnData: any; error?: infer TError; data: infer TData }
93
101
  ? UseQueryResult<TData, TError>
94
102
  : T extends { queryFnData: infer TQueryFnData; error?: infer TError }
95
- ? UseQueryResult<TQueryFnData, TError>
96
- : T extends { data: infer TData; error?: infer TError }
97
- ? UseQueryResult<TData, TError>
98
- : // Part 2: responsible for mapping explicit type parameter to function result, if tuple
99
- T extends [any, infer TError, infer TData]
100
- ? UseQueryResult<TData, TError>
101
- : T extends [infer TQueryFnData, infer TError]
102
- ? UseQueryResult<TQueryFnData, TError>
103
- : T extends [infer TQueryFnData]
104
- ? UseQueryResult<TQueryFnData>
105
- : // Part 3: responsible for mapping inferred type to results, if no explicit parameter was provided
106
- T extends {
107
- queryFn?: QueryFunction<unknown, any>
108
- select: (data: any) => infer TData
109
- throwOnError?: ThrowOnError<any, infer TError, any, any>
110
- }
111
- ? UseQueryResult<TData, unknown extends TError ? DefaultError : TError>
112
- : T extends {
113
- queryFn?: QueryFunction<infer TQueryFnData, any>
114
- throwOnError?: ThrowOnError<any, infer TError, any, any>
115
- }
116
- ? UseQueryResult<
117
- TQueryFnData,
118
- unknown extends TError ? DefaultError : TError
119
- >
120
- : // Fallback
121
- UseQueryResult
103
+ ? UseQueryResult<TQueryFnData, TError>
104
+ : T extends { data: infer TData; error?: infer TError }
105
+ ? UseQueryResult<TData, TError>
106
+ : // Part 2: responsible for mapping explicit type parameter to function result, if tuple
107
+ T extends [any, infer TError, infer TData]
108
+ ? UseQueryResult<TData, TError>
109
+ : T extends [infer TQueryFnData, infer TError]
110
+ ? UseQueryResult<TQueryFnData, TError>
111
+ : T extends [infer TQueryFnData]
112
+ ? UseQueryResult<TQueryFnData>
113
+ : // Part 3: responsible for mapping inferred type to results, if no explicit parameter was provided
114
+ T extends {
115
+ queryFn?: QueryFunction<unknown, any>
116
+ select: (data: any) => infer TData
117
+ throwOnError?: ThrowOnError<any, infer TError, any, any>
118
+ }
119
+ ? UseQueryResult<
120
+ TData,
121
+ unknown extends TError ? DefaultError : TError
122
+ >
123
+ : T extends {
124
+ queryFn?: QueryFunction<infer TQueryFnData, any>
125
+ throwOnError?: ThrowOnError<any, infer TError, any, any>
126
+ }
127
+ ? UseQueryResult<
128
+ TQueryFnData,
129
+ unknown extends TError ? DefaultError : TError
130
+ >
131
+ : // Fallback
132
+ UseQueryResult
122
133
 
123
134
  /**
124
135
  * QueriesOptions reducer recursively unwraps function arguments to infer/enforce type param
@@ -130,26 +141,37 @@ export type QueriesOptions<
130
141
  > = Depth['length'] extends MAXIMUM_DEPTH
131
142
  ? Array<UseQueryOptionsForUseQueries>
132
143
  : T extends []
133
- ? []
134
- : T extends [infer Head]
135
- ? [...Result, GetOptions<Head>]
136
- : T extends [infer Head, ...infer Tail]
137
- ? QueriesOptions<[...Tail], [...Result, GetOptions<Head>], [...Depth, 1]>
138
- : Array<unknown> extends T
139
- ? T
140
- : // If T is *some* array but we couldn't assign unknown[] to it, then it must hold some known/homogenous type!
141
- // use this to infer the param types in the case of Array.map() argument
142
- T extends Array<
143
- UseQueryOptionsForUseQueries<
144
- infer TQueryFnData,
145
- infer TError,
146
- infer TData,
147
- infer TQueryKey
148
- >
149
- >
150
- ? Array<UseQueryOptionsForUseQueries<TQueryFnData, TError, TData, TQueryKey>>
151
- : // Fallback
152
- Array<UseQueryOptionsForUseQueries>
144
+ ? []
145
+ : T extends [infer Head]
146
+ ? [...Result, GetOptions<Head>]
147
+ : T extends [infer Head, ...infer Tail]
148
+ ? QueriesOptions<
149
+ [...Tail],
150
+ [...Result, GetOptions<Head>],
151
+ [...Depth, 1]
152
+ >
153
+ : Array<unknown> extends T
154
+ ? T
155
+ : // If T is *some* array but we couldn't assign unknown[] to it, then it must hold some known/homogenous type!
156
+ // use this to infer the param types in the case of Array.map() argument
157
+ T extends Array<
158
+ UseQueryOptionsForUseQueries<
159
+ infer TQueryFnData,
160
+ infer TError,
161
+ infer TData,
162
+ infer TQueryKey
163
+ >
164
+ >
165
+ ? Array<
166
+ UseQueryOptionsForUseQueries<
167
+ TQueryFnData,
168
+ TError,
169
+ TData,
170
+ TQueryKey
171
+ >
172
+ >
173
+ : // Fallback
174
+ Array<UseQueryOptionsForUseQueries>
153
175
 
154
176
  /**
155
177
  * QueriesResults reducer recursively maps type param to results
@@ -161,28 +183,32 @@ export type QueriesResults<
161
183
  > = Depth['length'] extends MAXIMUM_DEPTH
162
184
  ? Array<UseQueryResult>
163
185
  : T extends []
164
- ? []
165
- : T extends [infer Head]
166
- ? [...Result, GetResults<Head>]
167
- : T extends [infer Head, ...infer Tail]
168
- ? QueriesResults<[...Tail], [...Result, GetResults<Head>], [...Depth, 1]>
169
- : T extends Array<
170
- UseQueryOptionsForUseQueries<
171
- infer TQueryFnData,
172
- infer TError,
173
- infer TData,
174
- any
175
- >
176
- >
177
- ? // Dynamic-size (homogenous) UseQueryOptions array: map directly to array of results
178
- Array<
179
- UseQueryResult<
180
- unknown extends TData ? TQueryFnData : TData,
181
- unknown extends TError ? DefaultError : TError
182
- >
183
- >
184
- : // Fallback
185
- Array<UseQueryResult>
186
+ ? []
187
+ : T extends [infer Head]
188
+ ? [...Result, GetResults<Head>]
189
+ : T extends [infer Head, ...infer Tail]
190
+ ? QueriesResults<
191
+ [...Tail],
192
+ [...Result, GetResults<Head>],
193
+ [...Depth, 1]
194
+ >
195
+ : T extends Array<
196
+ UseQueryOptionsForUseQueries<
197
+ infer TQueryFnData,
198
+ infer TError,
199
+ infer TData,
200
+ any
201
+ >
202
+ >
203
+ ? // Dynamic-size (homogenous) UseQueryOptions array: map directly to array of results
204
+ Array<
205
+ UseQueryResult<
206
+ unknown extends TData ? TQueryFnData : TData,
207
+ unknown extends TError ? DefaultError : TError
208
+ >
209
+ >
210
+ : // Fallback
211
+ Array<UseQueryResult>
186
212
 
187
213
  export function useQueries<
188
214
  T extends Array<any>,
@@ -260,7 +286,7 @@ export function useQueries<
260
286
  }, [defaultedQueries, options, observer])
261
287
 
262
288
  const shouldAtLeastOneSuspend = optimisticResult.some((result, index) =>
263
- shouldSuspend(defaultedQueries[index], result, isRestoring),
289
+ shouldSuspend(defaultedQueries[index], result),
264
290
  )
265
291
 
266
292
  const suspensePromises = shouldAtLeastOneSuspend
@@ -269,7 +295,7 @@ export function useQueries<
269
295
 
270
296
  if (opts) {
271
297
  const queryObserver = new QueryObserver(client, opts)
272
- if (shouldSuspend(opts, result, isRestoring)) {
298
+ if (shouldSuspend(opts, result)) {
273
299
  return fetchOptimistic(opts, queryObserver, errorResetBoundary)
274
300
  } else if (willFetch(result, isRestoring)) {
275
301
  void fetchOptimistic(opts, queryObserver, errorResetBoundary)
@@ -280,6 +306,13 @@ export function useQueries<
280
306
  : []
281
307
 
282
308
  if (suspensePromises.length > 0) {
309
+ observer.setQueries(
310
+ defaultedQueries,
311
+ options as QueriesObserverOptions<TCombinedResult>,
312
+ {
313
+ listeners: false,
314
+ },
315
+ )
283
316
  throw Promise.all(suspensePromises)
284
317
  }
285
318
  const observerQueries = observer.getQueries()