@tanstack/react-query 4.14.6 → 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/build/lib/suspense.d.ts +9 -0
- package/build/lib/suspense.esm.js +24 -0
- package/build/lib/suspense.esm.js.map +1 -0
- package/build/lib/suspense.js +31 -0
- package/build/lib/suspense.js.map +1 -0
- package/build/lib/suspense.mjs +24 -0
- package/build/lib/suspense.mjs.map +1 -0
- package/build/lib/useBaseQuery.esm.js +4 -19
- package/build/lib/useBaseQuery.esm.js.map +1 -1
- package/build/lib/useBaseQuery.js +4 -19
- package/build/lib/useBaseQuery.js.map +1 -1
- package/build/lib/useBaseQuery.mjs +4 -19
- package/build/lib/useBaseQuery.mjs.map +1 -1
- package/build/lib/useQueries.esm.js +26 -4
- package/build/lib/useQueries.esm.js.map +1 -1
- package/build/lib/useQueries.js +26 -4
- package/build/lib/useQueries.js.map +1 -1
- package/build/lib/useQueries.mjs +26 -4
- package/build/lib/useQueries.mjs.map +1 -1
- package/build/umd/index.development.js +54 -23
- package/build/umd/index.development.js.map +1 -1
- package/build/umd/index.production.js +1 -1
- package/build/umd/index.production.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/suspense.test.tsx +111 -0
- package/src/suspense.ts +59 -0
- package/src/useBaseQuery.ts +4 -25
- package/src/useQueries.ts +41 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/react-query",
|
|
3
|
-
"version": "4.
|
|
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.
|
|
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
|
+
})
|
package/src/suspense.ts
ADDED
|
@@ -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
|
+
})
|
package/src/useBaseQuery.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
|
246
|
+
return optimisticResult as QueriesResults<T>
|
|
215
247
|
}
|