@tanstack/react-query 4.14.5 → 4.15.0

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/react-query",
3
- "version": "4.14.5",
3
+ "version": "4.15.0",
4
4
  "description": "Hooks for managing, caching and syncing asynchronous and remote data in React",
5
5
  "author": "tannerlinsley",
6
6
  "license": "MIT",
@@ -46,7 +46,7 @@
46
46
  "react-error-boundary": "^3.1.4"
47
47
  },
48
48
  "dependencies": {
49
- "@tanstack/query-core": "4.14.5",
49
+ "@tanstack/query-core": "4.15.0",
50
50
  "use-sync-external-store": "^1.2.0"
51
51
  },
52
52
  "peerDependencies": {
@@ -8,6 +8,7 @@ import {
8
8
  QueryCache,
9
9
  QueryErrorResetBoundary,
10
10
  useInfiniteQuery,
11
+ useQueries,
11
12
  useQuery,
12
13
  useQueryErrorResetBoundary,
13
14
  } from '..'
@@ -1011,3 +1012,113 @@ describe("useQuery's in Suspense mode", () => {
1011
1012
  expect(rendered.queryByText('rendered')).not.toBeNull()
1012
1013
  })
1013
1014
  })
1015
+
1016
+ describe('useQueries with suspense', () => {
1017
+ const queryClient = createQueryClient()
1018
+ it('should suspend all queries in parallel', async () => {
1019
+ const key1 = queryKey()
1020
+ const key2 = queryKey()
1021
+ const results: string[] = []
1022
+
1023
+ function Fallback() {
1024
+ results.push('loading')
1025
+ return <div>loading</div>
1026
+ }
1027
+
1028
+ function Page() {
1029
+ const result = useQueries({
1030
+ queries: [
1031
+ {
1032
+ queryKey: key1,
1033
+ queryFn: async () => {
1034
+ results.push('1')
1035
+ await sleep(10)
1036
+ return '1'
1037
+ },
1038
+ suspense: true,
1039
+ },
1040
+ {
1041
+ queryKey: key2,
1042
+ queryFn: async () => {
1043
+ results.push('2')
1044
+ await sleep(20)
1045
+ return '2'
1046
+ },
1047
+ suspense: true,
1048
+ },
1049
+ ],
1050
+ })
1051
+ return (
1052
+ <div>
1053
+ <h1>data: {result.map((it) => it.data ?? 'null').join(',')}</h1>
1054
+ </div>
1055
+ )
1056
+ }
1057
+
1058
+ const rendered = renderWithClient(
1059
+ queryClient,
1060
+ <React.Suspense fallback={<Fallback />}>
1061
+ <Page />
1062
+ </React.Suspense>,
1063
+ )
1064
+ await waitFor(() => rendered.getByText('loading'))
1065
+ await waitFor(() => rendered.getByText('data: 1,2'))
1066
+
1067
+ expect(results).toEqual(['1', '2', 'loading'])
1068
+ })
1069
+
1070
+ it('should allow to mix suspense with non-suspense', async () => {
1071
+ const key1 = queryKey()
1072
+ const key2 = queryKey()
1073
+ const results: string[] = []
1074
+
1075
+ function Fallback() {
1076
+ results.push('loading')
1077
+ return <div>loading</div>
1078
+ }
1079
+
1080
+ function Page() {
1081
+ const result = useQueries({
1082
+ queries: [
1083
+ {
1084
+ queryKey: key1,
1085
+ queryFn: async () => {
1086
+ results.push('1')
1087
+ await sleep(10)
1088
+ return '1'
1089
+ },
1090
+ suspense: true,
1091
+ },
1092
+ {
1093
+ queryKey: key2,
1094
+ queryFn: async () => {
1095
+ results.push('2')
1096
+ await sleep(20)
1097
+ return '2'
1098
+ },
1099
+ suspense: false,
1100
+ },
1101
+ ],
1102
+ })
1103
+ return (
1104
+ <div>
1105
+ <h1>data: {result.map((it) => it.data ?? 'null').join(',')}</h1>
1106
+ <h2>status: {result.map((it) => it.status).join(',')}</h2>
1107
+ </div>
1108
+ )
1109
+ }
1110
+
1111
+ const rendered = renderWithClient(
1112
+ queryClient,
1113
+ <React.Suspense fallback={<Fallback />}>
1114
+ <Page />
1115
+ </React.Suspense>,
1116
+ )
1117
+ await waitFor(() => rendered.getByText('loading'))
1118
+ await waitFor(() => rendered.getByText('status: success,loading'))
1119
+ await waitFor(() => rendered.getByText('data: 1,null'))
1120
+ await waitFor(() => rendered.getByText('data: 1,2'))
1121
+
1122
+ expect(results).toEqual(['1', '2', 'loading'])
1123
+ })
1124
+ })
@@ -1024,7 +1024,7 @@ describe('useQuery', () => {
1024
1024
 
1025
1025
  return (
1026
1026
  <div>
1027
- <div>{state?.data}</div>
1027
+ <div>{state.data}</div>
1028
1028
  <button onClick={() => state.refetch()}>refetch</button>
1029
1029
  </div>
1030
1030
  )
@@ -0,0 +1,59 @@
1
+ import type { DefaultedQueryObserverOptions } from '@tanstack/query-core'
2
+ import type { QueryObserver } from '@tanstack/query-core'
3
+ import type { QueryErrorResetBoundaryValue } from './QueryErrorResetBoundary'
4
+ import type { QueryObserverResult } from '@tanstack/query-core'
5
+ import type { QueryKey } from '@tanstack/query-core'
6
+
7
+ export const ensureStaleTime = (
8
+ defaultedOptions: DefaultedQueryObserverOptions<any, any, any, any, any>,
9
+ ) => {
10
+ if (defaultedOptions.suspense) {
11
+ // Always set stale time when using suspense to prevent
12
+ // fetching again when directly mounting after suspending
13
+ if (typeof defaultedOptions.staleTime !== 'number') {
14
+ defaultedOptions.staleTime = 1000
15
+ }
16
+ }
17
+ }
18
+
19
+ export const willFetch = (
20
+ result: QueryObserverResult<any, any>,
21
+ isRestoring: boolean,
22
+ ) => result.isLoading && result.isFetching && !isRestoring
23
+
24
+ export const shouldSuspend = (
25
+ defaultedOptions:
26
+ | DefaultedQueryObserverOptions<any, any, any, any, any>
27
+ | undefined,
28
+ result: QueryObserverResult<any, any>,
29
+ isRestoring: boolean,
30
+ ) => defaultedOptions?.suspense && willFetch(result, isRestoring)
31
+
32
+ export const fetchOptimistic = <
33
+ TQueryFnData,
34
+ TError,
35
+ TData,
36
+ TQueryData,
37
+ TQueryKey extends QueryKey,
38
+ >(
39
+ defaultedOptions: DefaultedQueryObserverOptions<
40
+ TQueryFnData,
41
+ TError,
42
+ TData,
43
+ TQueryData,
44
+ TQueryKey
45
+ >,
46
+ observer: QueryObserver<TQueryFnData, TError, TData, TQueryData, TQueryKey>,
47
+ errorResetBoundary: QueryErrorResetBoundaryValue,
48
+ ) =>
49
+ observer
50
+ .fetchOptimistic(defaultedOptions)
51
+ .then(({ data }) => {
52
+ defaultedOptions.onSuccess?.(data as TData)
53
+ defaultedOptions.onSettled?.(data, null)
54
+ })
55
+ .catch((error) => {
56
+ errorResetBoundary.clearReset()
57
+ defaultedOptions.onError?.(error)
58
+ defaultedOptions.onSettled?.(undefined, error)
59
+ })
@@ -12,6 +12,7 @@ import {
12
12
  getHasError,
13
13
  useClearResetErrorBoundary,
14
14
  } from './errorBoundaryUtils'
15
+ import { ensureStaleTime, shouldSuspend, fetchOptimistic } from './suspense'
15
16
 
16
17
  export function useBaseQuery<
17
18
  TQueryFnData,
@@ -58,14 +59,7 @@ export function useBaseQuery<
58
59
  )
59
60
  }
60
61
 
61
- if (defaultedOptions.suspense) {
62
- // Always set stale time when using suspense to prevent
63
- // fetching again when directly mounting after suspending
64
- if (typeof defaultedOptions.staleTime !== 'number') {
65
- defaultedOptions.staleTime = 1000
66
- }
67
- }
68
-
62
+ ensureStaleTime(defaultedOptions)
69
63
  ensurePreventErrorBoundaryRetry(defaultedOptions, errorResetBoundary)
70
64
 
71
65
  useClearResetErrorBoundary(errorResetBoundary)
@@ -99,23 +93,8 @@ export function useBaseQuery<
99
93
  }, [defaultedOptions, observer])
100
94
 
101
95
  // Handle suspense
102
- if (
103
- defaultedOptions.suspense &&
104
- result.isLoading &&
105
- result.isFetching &&
106
- !isRestoring
107
- ) {
108
- throw observer
109
- .fetchOptimistic(defaultedOptions)
110
- .then(({ data }) => {
111
- defaultedOptions.onSuccess?.(data as TData)
112
- defaultedOptions.onSettled?.(data, null)
113
- })
114
- .catch((error) => {
115
- errorResetBoundary.clearReset()
116
- defaultedOptions.onError?.(error)
117
- defaultedOptions.onSettled?.(undefined, error)
118
- })
96
+ if (shouldSuspend(defaultedOptions, result, isRestoring)) {
97
+ throw fetchOptimistic(defaultedOptions, observer, errorResetBoundary)
119
98
  }
120
99
 
121
100
  // Handle error boundary
package/src/useQueries.ts CHANGED
@@ -12,6 +12,12 @@ import {
12
12
  getHasError,
13
13
  useClearResetErrorBoundary,
14
14
  } from './errorBoundaryUtils'
15
+ import {
16
+ ensureStaleTime,
17
+ shouldSuspend,
18
+ fetchOptimistic,
19
+ willFetch,
20
+ } from './suspense'
15
21
 
16
22
  // This defines the `UseQueryOptions` that are accepted in `QueriesOptions` & `GetOptions`.
17
23
  // - `context` is omitted as it is passed as a root-level option to `useQueries` instead.
@@ -170,7 +176,7 @@ export function useQueries<T extends any[]>({
170
176
  () => new QueriesObserver(queryClient, defaultedQueries),
171
177
  )
172
178
 
173
- const result = observer.getOptimisticResult(defaultedQueries)
179
+ const optimisticResult = observer.getOptimisticResult(defaultedQueries)
174
180
 
175
181
  useSyncExternalStore(
176
182
  React.useCallback(
@@ -194,22 +200,48 @@ export function useQueries<T extends any[]>({
194
200
 
195
201
  defaultedQueries.forEach((query) => {
196
202
  ensurePreventErrorBoundaryRetry(query, errorResetBoundary)
203
+ ensureStaleTime(query)
197
204
  })
198
205
 
199
206
  useClearResetErrorBoundary(errorResetBoundary)
200
207
 
201
- const firstSingleResultWhichShouldThrow = result.find((singleResult, index) =>
202
- getHasError({
203
- result: singleResult,
204
- errorResetBoundary,
205
- useErrorBoundary: defaultedQueries[index]?.useErrorBoundary ?? false,
206
- query: observer.getQueries()[index]!,
207
- }),
208
+ const shouldAtLeastOneSuspend = optimisticResult.some((result, index) =>
209
+ shouldSuspend(defaultedQueries[index], result, isRestoring),
210
+ )
211
+
212
+ const suspensePromises = shouldAtLeastOneSuspend
213
+ ? optimisticResult.flatMap((result, index) => {
214
+ const options = defaultedQueries[index]
215
+ const queryObserver = observer.getObservers()[index]
216
+
217
+ if (options && queryObserver) {
218
+ if (shouldSuspend(options, result, isRestoring)) {
219
+ return fetchOptimistic(options, queryObserver, errorResetBoundary)
220
+ } else if (willFetch(result, isRestoring)) {
221
+ void fetchOptimistic(options, queryObserver, errorResetBoundary)
222
+ }
223
+ }
224
+ return []
225
+ })
226
+ : []
227
+
228
+ if (suspensePromises.length > 0) {
229
+ throw Promise.all(suspensePromises)
230
+ }
231
+
232
+ const firstSingleResultWhichShouldThrow = optimisticResult.find(
233
+ (result, index) =>
234
+ getHasError({
235
+ result,
236
+ errorResetBoundary,
237
+ useErrorBoundary: defaultedQueries[index]?.useErrorBoundary ?? false,
238
+ query: observer.getQueries()[index]!,
239
+ }),
208
240
  )
209
241
 
210
242
  if (firstSingleResultWhichShouldThrow?.error) {
211
243
  throw firstSingleResultWhichShouldThrow.error
212
244
  }
213
245
 
214
- return result as QueriesResults<T>
246
+ return optimisticResult as QueriesResults<T>
215
247
  }