@tanstack/react-query 5.45.0 → 5.47.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/legacy/index.cjs +5 -0
- package/build/legacy/index.cjs.map +1 -1
- package/build/legacy/index.d.cts +1 -0
- package/build/legacy/index.d.ts +1 -0
- package/build/legacy/index.js +3 -0
- package/build/legacy/index.js.map +1 -1
- package/build/legacy/prefetch.cjs +45 -0
- package/build/legacy/prefetch.cjs.map +1 -0
- package/build/legacy/prefetch.d.cts +6 -0
- package/build/legacy/prefetch.d.ts +6 -0
- package/build/legacy/prefetch.js +19 -0
- package/build/legacy/prefetch.js.map +1 -0
- package/build/legacy/useSuspenseQueries.cjs.map +1 -1
- package/build/legacy/useSuspenseQueries.d.cts +6 -5
- package/build/legacy/useSuspenseQueries.d.ts +6 -5
- package/build/legacy/useSuspenseQueries.js.map +1 -1
- package/build/modern/index.cjs +5 -0
- package/build/modern/index.cjs.map +1 -1
- package/build/modern/index.d.cts +1 -0
- package/build/modern/index.d.ts +1 -0
- package/build/modern/index.js +3 -0
- package/build/modern/index.js.map +1 -1
- package/build/modern/prefetch.cjs +45 -0
- package/build/modern/prefetch.cjs.map +1 -0
- package/build/modern/prefetch.d.cts +6 -0
- package/build/modern/prefetch.d.ts +6 -0
- package/build/modern/prefetch.js +19 -0
- package/build/modern/prefetch.js.map +1 -0
- package/build/modern/useSuspenseQueries.cjs.map +1 -1
- package/build/modern/useSuspenseQueries.d.cts +6 -5
- package/build/modern/useSuspenseQueries.d.ts +6 -5
- package/build/modern/useSuspenseQueries.js.map +1 -1
- package/build/query-codemods/tsconfig.json +4 -1
- package/package.json +2 -2
- package/src/__tests__/prefetch.test-d.tsx +80 -0
- package/src/__tests__/prefetch.test.tsx +434 -0
- package/src/__tests__/useSuspenseQueries.test-d.tsx +20 -2
- package/src/index.ts +1 -0
- package/src/prefetch.ts +42 -0
- package/src/useSuspenseQueries.ts +9 -5
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { UseSuspenseQueryOptions, UseSuspenseQueryResult } from './types.cjs';
|
|
2
|
-
import { DefaultError, QueryClient, QueryFunction,
|
|
2
|
+
import { DefaultError, QueryClient, QueryFunction, ThrowOnError } from '@tanstack/query-core';
|
|
3
3
|
|
|
4
4
|
type MAXIMUM_DEPTH = 20;
|
|
5
|
+
type SkipTokenForUseQueries = symbol;
|
|
5
6
|
type GetUseSuspenseQueryOptions<T> = T extends {
|
|
6
7
|
queryFnData: infer TQueryFnData;
|
|
7
8
|
error?: infer TError;
|
|
@@ -13,11 +14,11 @@ type GetUseSuspenseQueryOptions<T> = T extends {
|
|
|
13
14
|
data: infer TData;
|
|
14
15
|
error?: infer TError;
|
|
15
16
|
} ? UseSuspenseQueryOptions<unknown, TError, TData> : T extends [infer TQueryFnData, infer TError, infer TData] ? UseSuspenseQueryOptions<TQueryFnData, TError, TData> : T extends [infer TQueryFnData, infer TError] ? UseSuspenseQueryOptions<TQueryFnData, TError> : T extends [infer TQueryFnData] ? UseSuspenseQueryOptions<TQueryFnData> : T extends {
|
|
16
|
-
queryFn?: QueryFunction<infer TQueryFnData, infer TQueryKey> |
|
|
17
|
+
queryFn?: QueryFunction<infer TQueryFnData, infer TQueryKey> | SkipTokenForUseQueries;
|
|
17
18
|
select?: (data: any) => infer TData;
|
|
18
19
|
throwOnError?: ThrowOnError<any, infer TError, any, any>;
|
|
19
20
|
} ? UseSuspenseQueryOptions<TQueryFnData, TError, TData, TQueryKey> : T extends {
|
|
20
|
-
queryFn?: QueryFunction<infer TQueryFnData, infer TQueryKey> |
|
|
21
|
+
queryFn?: QueryFunction<infer TQueryFnData, infer TQueryKey> | SkipTokenForUseQueries;
|
|
21
22
|
throwOnError?: ThrowOnError<any, infer TError, any, any>;
|
|
22
23
|
} ? UseSuspenseQueryOptions<TQueryFnData, TError, TQueryFnData, TQueryKey> : UseSuspenseQueryOptions;
|
|
23
24
|
type GetUseSuspenseQueryResult<T> = T extends {
|
|
@@ -31,11 +32,11 @@ type GetUseSuspenseQueryResult<T> = T extends {
|
|
|
31
32
|
data: infer TData;
|
|
32
33
|
error?: infer TError;
|
|
33
34
|
} ? UseSuspenseQueryResult<TData, TError> : T extends [any, infer TError, infer TData] ? UseSuspenseQueryResult<TData, TError> : T extends [infer TQueryFnData, infer TError] ? UseSuspenseQueryResult<TQueryFnData, TError> : T extends [infer TQueryFnData] ? UseSuspenseQueryResult<TQueryFnData> : T extends {
|
|
34
|
-
queryFn?: QueryFunction<infer TQueryFnData, any> |
|
|
35
|
+
queryFn?: QueryFunction<infer TQueryFnData, any> | SkipTokenForUseQueries;
|
|
35
36
|
select?: (data: any) => infer TData;
|
|
36
37
|
throwOnError?: ThrowOnError<any, infer TError, any, any>;
|
|
37
38
|
} ? UseSuspenseQueryResult<unknown extends TData ? TQueryFnData : TData, unknown extends TError ? DefaultError : TError> : T extends {
|
|
38
|
-
queryFn?: QueryFunction<infer TQueryFnData, any> |
|
|
39
|
+
queryFn?: QueryFunction<infer TQueryFnData, any> | SkipTokenForUseQueries;
|
|
39
40
|
throwOnError?: ThrowOnError<any, infer TError, any, any>;
|
|
40
41
|
} ? UseSuspenseQueryResult<TQueryFnData, unknown extends TError ? DefaultError : TError> : UseSuspenseQueryResult;
|
|
41
42
|
/**
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { UseSuspenseQueryOptions, UseSuspenseQueryResult } from './types.js';
|
|
2
|
-
import { DefaultError, QueryClient, QueryFunction,
|
|
2
|
+
import { DefaultError, QueryClient, QueryFunction, ThrowOnError } from '@tanstack/query-core';
|
|
3
3
|
|
|
4
4
|
type MAXIMUM_DEPTH = 20;
|
|
5
|
+
type SkipTokenForUseQueries = symbol;
|
|
5
6
|
type GetUseSuspenseQueryOptions<T> = T extends {
|
|
6
7
|
queryFnData: infer TQueryFnData;
|
|
7
8
|
error?: infer TError;
|
|
@@ -13,11 +14,11 @@ type GetUseSuspenseQueryOptions<T> = T extends {
|
|
|
13
14
|
data: infer TData;
|
|
14
15
|
error?: infer TError;
|
|
15
16
|
} ? UseSuspenseQueryOptions<unknown, TError, TData> : T extends [infer TQueryFnData, infer TError, infer TData] ? UseSuspenseQueryOptions<TQueryFnData, TError, TData> : T extends [infer TQueryFnData, infer TError] ? UseSuspenseQueryOptions<TQueryFnData, TError> : T extends [infer TQueryFnData] ? UseSuspenseQueryOptions<TQueryFnData> : T extends {
|
|
16
|
-
queryFn?: QueryFunction<infer TQueryFnData, infer TQueryKey> |
|
|
17
|
+
queryFn?: QueryFunction<infer TQueryFnData, infer TQueryKey> | SkipTokenForUseQueries;
|
|
17
18
|
select?: (data: any) => infer TData;
|
|
18
19
|
throwOnError?: ThrowOnError<any, infer TError, any, any>;
|
|
19
20
|
} ? UseSuspenseQueryOptions<TQueryFnData, TError, TData, TQueryKey> : T extends {
|
|
20
|
-
queryFn?: QueryFunction<infer TQueryFnData, infer TQueryKey> |
|
|
21
|
+
queryFn?: QueryFunction<infer TQueryFnData, infer TQueryKey> | SkipTokenForUseQueries;
|
|
21
22
|
throwOnError?: ThrowOnError<any, infer TError, any, any>;
|
|
22
23
|
} ? UseSuspenseQueryOptions<TQueryFnData, TError, TQueryFnData, TQueryKey> : UseSuspenseQueryOptions;
|
|
23
24
|
type GetUseSuspenseQueryResult<T> = T extends {
|
|
@@ -31,11 +32,11 @@ type GetUseSuspenseQueryResult<T> = T extends {
|
|
|
31
32
|
data: infer TData;
|
|
32
33
|
error?: infer TError;
|
|
33
34
|
} ? UseSuspenseQueryResult<TData, TError> : T extends [any, infer TError, infer TData] ? UseSuspenseQueryResult<TData, TError> : T extends [infer TQueryFnData, infer TError] ? UseSuspenseQueryResult<TQueryFnData, TError> : T extends [infer TQueryFnData] ? UseSuspenseQueryResult<TQueryFnData> : T extends {
|
|
34
|
-
queryFn?: QueryFunction<infer TQueryFnData, any> |
|
|
35
|
+
queryFn?: QueryFunction<infer TQueryFnData, any> | SkipTokenForUseQueries;
|
|
35
36
|
select?: (data: any) => infer TData;
|
|
36
37
|
throwOnError?: ThrowOnError<any, infer TError, any, any>;
|
|
37
38
|
} ? UseSuspenseQueryResult<unknown extends TData ? TQueryFnData : TData, unknown extends TError ? DefaultError : TError> : T extends {
|
|
38
|
-
queryFn?: QueryFunction<infer TQueryFnData, any> |
|
|
39
|
+
queryFn?: QueryFunction<infer TQueryFnData, any> | SkipTokenForUseQueries;
|
|
39
40
|
throwOnError?: ThrowOnError<any, infer TError, any, any>;
|
|
40
41
|
} ? UseSuspenseQueryResult<TQueryFnData, unknown extends TError ? DefaultError : TError> : UseSuspenseQueryResult;
|
|
41
42
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/useSuspenseQueries.ts"],"sourcesContent":["'use client'\nimport { useQueries } from './useQueries'\nimport { defaultThrowOnError } from './suspense'\nimport type { UseSuspenseQueryOptions, UseSuspenseQueryResult } from './types'\nimport type {\n DefaultError,\n QueryClient,\n QueryFunction,\n
|
|
1
|
+
{"version":3,"sources":["../../src/useSuspenseQueries.ts"],"sourcesContent":["'use client'\nimport { useQueries } from './useQueries'\nimport { defaultThrowOnError } from './suspense'\nimport type { UseSuspenseQueryOptions, UseSuspenseQueryResult } from './types'\nimport type {\n DefaultError,\n QueryClient,\n QueryFunction,\n ThrowOnError,\n} from '@tanstack/query-core'\n\n// Avoid TS depth-limit error in case of large array literal\ntype MAXIMUM_DEPTH = 20\n\n// Widen the type of the symbol to enable type inference even if skipToken is not immutable.\ntype SkipTokenForUseQueries = symbol\n\ntype GetUseSuspenseQueryOptions<T> =\n // Part 1: responsible for applying explicit type parameter to function arguments, if object { queryFnData: TQueryFnData, error: TError, data: TData }\n T extends {\n queryFnData: infer TQueryFnData\n error?: infer TError\n data: infer TData\n }\n ? UseSuspenseQueryOptions<TQueryFnData, TError, TData>\n : T extends { queryFnData: infer TQueryFnData; error?: infer TError }\n ? UseSuspenseQueryOptions<TQueryFnData, TError>\n : T extends { data: infer TData; error?: infer TError }\n ? UseSuspenseQueryOptions<unknown, TError, TData>\n : // Part 2: responsible for applying explicit type parameter to function arguments, if tuple [TQueryFnData, TError, TData]\n T extends [infer TQueryFnData, infer TError, infer TData]\n ? UseSuspenseQueryOptions<TQueryFnData, TError, TData>\n : T extends [infer TQueryFnData, infer TError]\n ? UseSuspenseQueryOptions<TQueryFnData, TError>\n : T extends [infer TQueryFnData]\n ? UseSuspenseQueryOptions<TQueryFnData>\n : // Part 3: responsible for inferring and enforcing type if no explicit parameter was provided\n T extends {\n queryFn?:\n | QueryFunction<infer TQueryFnData, infer TQueryKey>\n | SkipTokenForUseQueries\n select?: (data: any) => infer TData\n throwOnError?: ThrowOnError<any, infer TError, any, any>\n }\n ? UseSuspenseQueryOptions<\n TQueryFnData,\n TError,\n TData,\n TQueryKey\n >\n : T extends {\n queryFn?:\n | QueryFunction<infer TQueryFnData, infer TQueryKey>\n | SkipTokenForUseQueries\n throwOnError?: ThrowOnError<any, infer TError, any, any>\n }\n ? UseSuspenseQueryOptions<\n TQueryFnData,\n TError,\n TQueryFnData,\n TQueryKey\n >\n : // Fallback\n UseSuspenseQueryOptions\n\ntype GetUseSuspenseQueryResult<T> =\n // Part 1: responsible for mapping explicit type parameter to function result, if object\n T extends { queryFnData: any; error?: infer TError; data: infer TData }\n ? UseSuspenseQueryResult<TData, TError>\n : T extends { queryFnData: infer TQueryFnData; error?: infer TError }\n ? UseSuspenseQueryResult<TQueryFnData, TError>\n : T extends { data: infer TData; error?: infer TError }\n ? UseSuspenseQueryResult<TData, TError>\n : // Part 2: responsible for mapping explicit type parameter to function result, if tuple\n T extends [any, infer TError, infer TData]\n ? UseSuspenseQueryResult<TData, TError>\n : T extends [infer TQueryFnData, infer TError]\n ? UseSuspenseQueryResult<TQueryFnData, TError>\n : T extends [infer TQueryFnData]\n ? UseSuspenseQueryResult<TQueryFnData>\n : // Part 3: responsible for mapping inferred type to results, if no explicit parameter was provided\n T extends {\n queryFn?:\n | QueryFunction<infer TQueryFnData, any>\n | SkipTokenForUseQueries\n select?: (data: any) => infer TData\n throwOnError?: ThrowOnError<any, infer TError, any, any>\n }\n ? UseSuspenseQueryResult<\n unknown extends TData ? TQueryFnData : TData,\n unknown extends TError ? DefaultError : TError\n >\n : T extends {\n queryFn?:\n | QueryFunction<infer TQueryFnData, any>\n | SkipTokenForUseQueries\n throwOnError?: ThrowOnError<any, infer TError, any, any>\n }\n ? UseSuspenseQueryResult<\n TQueryFnData,\n unknown extends TError ? DefaultError : TError\n >\n : // Fallback\n UseSuspenseQueryResult\n\n/**\n * SuspenseQueriesOptions reducer recursively unwraps function arguments to infer/enforce type param\n */\nexport type SuspenseQueriesOptions<\n T extends Array<any>,\n TResults extends Array<any> = [],\n TDepth extends ReadonlyArray<number> = [],\n> = TDepth['length'] extends MAXIMUM_DEPTH\n ? Array<UseSuspenseQueryOptions>\n : T extends []\n ? []\n : T extends [infer Head]\n ? [...TResults, GetUseSuspenseQueryOptions<Head>]\n : T extends [infer Head, ...infer Tails]\n ? SuspenseQueriesOptions<\n [...Tails],\n [...TResults, GetUseSuspenseQueryOptions<Head>],\n [...TDepth, 1]\n >\n : Array<unknown> extends T\n ? T\n : // If T is *some* array but we couldn't assign unknown[] to it, then it must hold some known/homogenous type!\n // use this to infer the param types in the case of Array.map() argument\n T extends Array<\n UseSuspenseQueryOptions<\n infer TQueryFnData,\n infer TError,\n infer TData,\n infer TQueryKey\n >\n >\n ? Array<\n UseSuspenseQueryOptions<TQueryFnData, TError, TData, TQueryKey>\n >\n : // Fallback\n Array<UseSuspenseQueryOptions>\n\n/**\n * SuspenseQueriesResults reducer recursively maps type param to results\n */\nexport type SuspenseQueriesResults<\n T extends Array<any>,\n TResults extends Array<any> = [],\n TDepth extends ReadonlyArray<number> = [],\n> = TDepth['length'] extends MAXIMUM_DEPTH\n ? Array<UseSuspenseQueryResult>\n : T extends []\n ? []\n : T extends [infer Head]\n ? [...TResults, GetUseSuspenseQueryResult<Head>]\n : T extends [infer Head, ...infer Tails]\n ? SuspenseQueriesResults<\n [...Tails],\n [...TResults, GetUseSuspenseQueryResult<Head>],\n [...TDepth, 1]\n >\n : T extends Array<\n UseSuspenseQueryOptions<\n infer TQueryFnData,\n infer TError,\n infer TData,\n any\n >\n >\n ? // Dynamic-size (homogenous) UseQueryOptions array: map directly to array of results\n Array<\n UseSuspenseQueryResult<\n unknown extends TData ? TQueryFnData : TData,\n unknown extends TError ? DefaultError : TError\n >\n >\n : // Fallback\n Array<UseSuspenseQueryResult>\n\nexport function useSuspenseQueries<\n T extends Array<any>,\n TCombinedResult = SuspenseQueriesResults<T>,\n>(\n options: {\n queries: readonly [...SuspenseQueriesOptions<T>]\n combine?: (result: SuspenseQueriesResults<T>) => TCombinedResult\n },\n queryClient?: QueryClient,\n): TCombinedResult {\n return useQueries(\n {\n ...options,\n queries: options.queries.map((query) => ({\n ...query,\n suspense: true,\n throwOnError: defaultThrowOnError,\n enabled: true,\n placeholderData: undefined,\n })),\n } as any,\n queryClient,\n )\n}\n"],"mappings":";;;AACA,SAAS,kBAAkB;AAC3B,SAAS,2BAA2B;AAiL7B,SAAS,mBAId,SAIA,aACiB;AACjB,SAAO;AAAA,IACL;AAAA,MACE,GAAG;AAAA,MACH,SAAS,QAAQ,QAAQ,IAAI,CAAC,WAAW;AAAA,QACvC,GAAG;AAAA,QACH,UAAU;AAAA,QACV,cAAc;AAAA,QACd,SAAS;AAAA,QACT,iBAAiB;AAAA,MACnB,EAAE;AAAA,IACJ;AAAA,IACA;AAAA,EACF;AACF;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/react-query",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.47.0",
|
|
4
4
|
"description": "Hooks for managing, caching and syncing asynchronous and remote data in React",
|
|
5
5
|
"author": "tannerlinsley",
|
|
6
6
|
"license": "MIT",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"!build/codemods/**/__tests__"
|
|
42
42
|
],
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@tanstack/query-core": "5.
|
|
44
|
+
"@tanstack/query-core": "5.47.0"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@types/react": "npm:types-react@rc",
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { describe, expectTypeOf, it } from 'vitest'
|
|
2
|
+
import { usePrefetchInfiniteQuery, usePrefetchQuery } from '../prefetch'
|
|
3
|
+
|
|
4
|
+
describe('usePrefetchQuery', () => {
|
|
5
|
+
it('should return nothing', () => {
|
|
6
|
+
const result = usePrefetchQuery({
|
|
7
|
+
queryKey: ['key'],
|
|
8
|
+
queryFn: () => Promise.resolve(5),
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
expectTypeOf(result).toEqualTypeOf<void>()
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('should not allow refetchInterval, enabled or throwOnError options', () => {
|
|
15
|
+
usePrefetchQuery({
|
|
16
|
+
queryKey: ['key'],
|
|
17
|
+
queryFn: () => Promise.resolve(5),
|
|
18
|
+
// @ts-expect-error TS2345
|
|
19
|
+
refetchInterval: 1000,
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
usePrefetchQuery({
|
|
23
|
+
queryKey: ['key'],
|
|
24
|
+
queryFn: () => Promise.resolve(5),
|
|
25
|
+
// @ts-expect-error TS2345
|
|
26
|
+
enabled: true,
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
usePrefetchQuery({
|
|
30
|
+
queryKey: ['key'],
|
|
31
|
+
queryFn: () => Promise.resolve(5),
|
|
32
|
+
// @ts-expect-error TS2345
|
|
33
|
+
throwOnError: true,
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
describe('useInfinitePrefetchQuery', () => {
|
|
39
|
+
it('should return nothing', () => {
|
|
40
|
+
const result = usePrefetchInfiniteQuery({
|
|
41
|
+
queryKey: ['key'],
|
|
42
|
+
queryFn: () => Promise.resolve(5),
|
|
43
|
+
initialPageParam: 1,
|
|
44
|
+
getNextPageParam: () => 1,
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
expectTypeOf(result).toEqualTypeOf<void>()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('should require initialPageParam and getNextPageParam', () => {
|
|
51
|
+
// @ts-expect-error TS2345
|
|
52
|
+
usePrefetchInfiniteQuery({
|
|
53
|
+
queryKey: ['key'],
|
|
54
|
+
queryFn: () => Promise.resolve(5),
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('should not allow refetchInterval, enabled or throwOnError options', () => {
|
|
59
|
+
usePrefetchQuery({
|
|
60
|
+
queryKey: ['key'],
|
|
61
|
+
queryFn: () => Promise.resolve(5),
|
|
62
|
+
// @ts-expect-error TS2345
|
|
63
|
+
refetchInterval: 1000,
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
usePrefetchQuery({
|
|
67
|
+
queryKey: ['key'],
|
|
68
|
+
queryFn: () => Promise.resolve(5),
|
|
69
|
+
// @ts-expect-error TS2345
|
|
70
|
+
enabled: true,
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
usePrefetchQuery({
|
|
74
|
+
queryKey: ['key'],
|
|
75
|
+
queryFn: () => Promise.resolve(5),
|
|
76
|
+
// @ts-expect-error TS2345
|
|
77
|
+
throwOnError: true,
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
})
|
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import { fireEvent, waitFor } from '@testing-library/react'
|
|
4
|
+
import { ErrorBoundary } from 'react-error-boundary'
|
|
5
|
+
import {
|
|
6
|
+
QueryCache,
|
|
7
|
+
usePrefetchInfiniteQuery,
|
|
8
|
+
usePrefetchQuery,
|
|
9
|
+
useQueryErrorResetBoundary,
|
|
10
|
+
useSuspenseInfiniteQuery,
|
|
11
|
+
useSuspenseQuery,
|
|
12
|
+
} from '..'
|
|
13
|
+
import { createQueryClient, queryKey, renderWithClient, sleep } from './utils'
|
|
14
|
+
|
|
15
|
+
import type { InfiniteData, UseInfiniteQueryOptions, UseQueryOptions } from '..'
|
|
16
|
+
import type { Mock } from 'vitest'
|
|
17
|
+
|
|
18
|
+
const generateQueryFn = (data: string) =>
|
|
19
|
+
vi.fn<any, Promise<string>>().mockImplementation(async () => {
|
|
20
|
+
await sleep(10)
|
|
21
|
+
|
|
22
|
+
return data
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const generateInfiniteQueryOptions = (
|
|
26
|
+
data: Array<{ data: string; currentPage: number; totalPages: number }>,
|
|
27
|
+
) => {
|
|
28
|
+
let currentPage = 0
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
queryFn: vi
|
|
32
|
+
.fn<any, Promise<(typeof data)[number]>>()
|
|
33
|
+
.mockImplementation(async () => {
|
|
34
|
+
const currentPageData = data[currentPage]
|
|
35
|
+
if (!currentPageData) {
|
|
36
|
+
throw new Error('No data defined for page ' + currentPage)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
await sleep(10)
|
|
40
|
+
currentPage++
|
|
41
|
+
|
|
42
|
+
return currentPageData
|
|
43
|
+
}),
|
|
44
|
+
initialPageParam: 1,
|
|
45
|
+
getNextPageParam: (lastPage: (typeof data)[number]) =>
|
|
46
|
+
lastPage.currentPage === lastPage.totalPages
|
|
47
|
+
? undefined
|
|
48
|
+
: lastPage.currentPage + 1,
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe('usePrefetchQuery', () => {
|
|
53
|
+
const queryCache = new QueryCache()
|
|
54
|
+
const queryClient = createQueryClient({ queryCache })
|
|
55
|
+
|
|
56
|
+
function Suspended<TData = unknown>(props: {
|
|
57
|
+
queryOpts: UseQueryOptions<TData, Error, TData, Array<string>>
|
|
58
|
+
children?: React.ReactNode
|
|
59
|
+
}) {
|
|
60
|
+
const state = useSuspenseQuery(props.queryOpts)
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div>
|
|
64
|
+
<div>data: {String(state.data)}</div>
|
|
65
|
+
{props.children}
|
|
66
|
+
</div>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
it('should prefetch query if query state does not exist', async () => {
|
|
71
|
+
const queryOpts = {
|
|
72
|
+
queryKey: queryKey(),
|
|
73
|
+
queryFn: generateQueryFn('prefetchQuery'),
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const componentQueryOpts = {
|
|
77
|
+
...queryOpts,
|
|
78
|
+
queryFn: generateQueryFn('useSuspenseQuery'),
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function App() {
|
|
82
|
+
usePrefetchQuery(queryOpts)
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<React.Suspense fallback="Loading...">
|
|
86
|
+
<Suspended queryOpts={componentQueryOpts} />
|
|
87
|
+
</React.Suspense>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const rendered = renderWithClient(queryClient, <App />)
|
|
92
|
+
|
|
93
|
+
await waitFor(() => rendered.getByText('data: prefetchQuery'))
|
|
94
|
+
expect(queryOpts.queryFn).toHaveBeenCalledTimes(1)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('should not prefetch query if query state exists', async () => {
|
|
98
|
+
const queryOpts = {
|
|
99
|
+
queryKey: queryKey(),
|
|
100
|
+
queryFn: generateQueryFn('The usePrefetchQuery hook is smart!'),
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function App() {
|
|
104
|
+
usePrefetchQuery(queryOpts)
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<React.Suspense fallback="Loading...">
|
|
108
|
+
<Suspended queryOpts={queryOpts} />
|
|
109
|
+
</React.Suspense>
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
await queryClient.fetchQuery(queryOpts)
|
|
114
|
+
queryOpts.queryFn.mockClear()
|
|
115
|
+
const rendered = renderWithClient(queryClient, <App />)
|
|
116
|
+
|
|
117
|
+
expect(rendered.queryByText('fetching: true')).not.toBeInTheDocument()
|
|
118
|
+
await waitFor(() =>
|
|
119
|
+
rendered.getByText('data: The usePrefetchQuery hook is smart!'),
|
|
120
|
+
)
|
|
121
|
+
expect(queryOpts.queryFn).not.toHaveBeenCalled()
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('should let errors fall through and not refetch failed queries', async () => {
|
|
125
|
+
const queryFn = generateQueryFn('Not an error')
|
|
126
|
+
|
|
127
|
+
const queryOpts = {
|
|
128
|
+
queryKey: queryKey(),
|
|
129
|
+
queryFn,
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
queryFn.mockImplementationOnce(async () => {
|
|
133
|
+
await sleep(10)
|
|
134
|
+
|
|
135
|
+
throw new Error('Oops! Server error!')
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
function App() {
|
|
139
|
+
usePrefetchQuery(queryOpts)
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<ErrorBoundary fallbackRender={() => <div>Oops!</div>}>
|
|
143
|
+
<React.Suspense fallback="Loading...">
|
|
144
|
+
<Suspended queryOpts={queryOpts} />
|
|
145
|
+
</React.Suspense>
|
|
146
|
+
</ErrorBoundary>
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
await queryClient.prefetchQuery(queryOpts)
|
|
151
|
+
queryFn.mockClear()
|
|
152
|
+
const rendered = renderWithClient(queryClient, <App />)
|
|
153
|
+
|
|
154
|
+
await waitFor(() => rendered.getByText('Oops!'))
|
|
155
|
+
expect(rendered.queryByText('data: Not an error')).not.toBeInTheDocument()
|
|
156
|
+
expect(queryOpts.queryFn).not.toHaveBeenCalled()
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('should not create an endless loop when using inside a suspense boundary', async () => {
|
|
160
|
+
const queryFn = generateQueryFn('prefetchedQuery')
|
|
161
|
+
|
|
162
|
+
const queryOpts = {
|
|
163
|
+
queryKey: queryKey(),
|
|
164
|
+
queryFn,
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function Prefetch({ children }: { children: React.ReactNode }) {
|
|
168
|
+
usePrefetchQuery(queryOpts)
|
|
169
|
+
return <>{children}</>
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function App() {
|
|
173
|
+
return (
|
|
174
|
+
<React.Suspense>
|
|
175
|
+
<Prefetch>
|
|
176
|
+
<Suspended queryOpts={queryOpts} />
|
|
177
|
+
</Prefetch>
|
|
178
|
+
</React.Suspense>
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const rendered = renderWithClient(queryClient, <App />)
|
|
183
|
+
await waitFor(() => rendered.getByText('data: prefetchedQuery'))
|
|
184
|
+
expect(queryOpts.queryFn).toHaveBeenCalledTimes(1)
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('should be able to recover from errors and try fetching again', async () => {
|
|
188
|
+
const queryFn = generateQueryFn('This is fine :dog: :fire:')
|
|
189
|
+
|
|
190
|
+
const queryOpts = {
|
|
191
|
+
queryKey: queryKey(),
|
|
192
|
+
queryFn,
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
queryFn.mockImplementationOnce(async () => {
|
|
196
|
+
await sleep(10)
|
|
197
|
+
|
|
198
|
+
throw new Error('Oops! Server error!')
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
function App() {
|
|
202
|
+
const { reset } = useQueryErrorResetBoundary()
|
|
203
|
+
usePrefetchQuery(queryOpts)
|
|
204
|
+
|
|
205
|
+
return (
|
|
206
|
+
<ErrorBoundary
|
|
207
|
+
onReset={reset}
|
|
208
|
+
fallbackRender={({ resetErrorBoundary }) => (
|
|
209
|
+
<div>
|
|
210
|
+
<div>Oops!</div>
|
|
211
|
+
<button onClick={resetErrorBoundary}>Try again</button>
|
|
212
|
+
</div>
|
|
213
|
+
)}
|
|
214
|
+
>
|
|
215
|
+
<React.Suspense fallback="Loading...">
|
|
216
|
+
<Suspended queryOpts={queryOpts} />
|
|
217
|
+
</React.Suspense>
|
|
218
|
+
</ErrorBoundary>
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
await queryClient.prefetchQuery(queryOpts)
|
|
223
|
+
queryFn.mockClear()
|
|
224
|
+
|
|
225
|
+
const rendered = renderWithClient(queryClient, <App />)
|
|
226
|
+
|
|
227
|
+
await waitFor(() => rendered.getByText('Oops!'))
|
|
228
|
+
fireEvent.click(rendered.getByText('Try again'))
|
|
229
|
+
await waitFor(() => rendered.getByText('data: This is fine :dog: :fire:'))
|
|
230
|
+
expect(queryOpts.queryFn).toHaveBeenCalledTimes(1)
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
it('should not create a suspense waterfall if prefetch is fired', async () => {
|
|
234
|
+
const firstQueryOpts = {
|
|
235
|
+
queryKey: queryKey(),
|
|
236
|
+
queryFn: generateQueryFn('Prefetch is nice!'),
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const secondQueryOpts = {
|
|
240
|
+
queryKey: queryKey(),
|
|
241
|
+
queryFn: generateQueryFn('Prefetch is really nice!!'),
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const thirdQueryOpts = {
|
|
245
|
+
queryKey: queryKey(),
|
|
246
|
+
queryFn: generateQueryFn('Prefetch does not create waterfalls!!'),
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const Fallback = vi.fn().mockImplementation(() => <div>Loading...</div>)
|
|
250
|
+
|
|
251
|
+
function App() {
|
|
252
|
+
usePrefetchQuery(firstQueryOpts)
|
|
253
|
+
usePrefetchQuery(secondQueryOpts)
|
|
254
|
+
usePrefetchQuery(thirdQueryOpts)
|
|
255
|
+
|
|
256
|
+
return (
|
|
257
|
+
<React.Suspense fallback={<Fallback />}>
|
|
258
|
+
<Suspended queryOpts={firstQueryOpts}>
|
|
259
|
+
<Suspended queryOpts={secondQueryOpts}>
|
|
260
|
+
<Suspended queryOpts={thirdQueryOpts} />
|
|
261
|
+
</Suspended>
|
|
262
|
+
</Suspended>
|
|
263
|
+
</React.Suspense>
|
|
264
|
+
)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const rendered = renderWithClient(queryClient, <App />)
|
|
268
|
+
expect(
|
|
269
|
+
queryClient.getQueryState(firstQueryOpts.queryKey)?.fetchStatus,
|
|
270
|
+
).toBe('fetching')
|
|
271
|
+
expect(
|
|
272
|
+
queryClient.getQueryState(secondQueryOpts.queryKey)?.fetchStatus,
|
|
273
|
+
).toBe('fetching')
|
|
274
|
+
expect(
|
|
275
|
+
queryClient.getQueryState(thirdQueryOpts.queryKey)?.fetchStatus,
|
|
276
|
+
).toBe('fetching')
|
|
277
|
+
await waitFor(() => rendered.getByText('Loading...'))
|
|
278
|
+
await waitFor(() => rendered.getByText('data: Prefetch is nice!'))
|
|
279
|
+
await waitFor(() => rendered.getByText('data: Prefetch is really nice!!'))
|
|
280
|
+
await waitFor(() =>
|
|
281
|
+
rendered.getByText('data: Prefetch does not create waterfalls!!'),
|
|
282
|
+
)
|
|
283
|
+
expect(Fallback).toHaveBeenCalledTimes(1)
|
|
284
|
+
expect(firstQueryOpts.queryFn).toHaveBeenCalledTimes(1)
|
|
285
|
+
expect(secondQueryOpts.queryFn).toHaveBeenCalledTimes(1)
|
|
286
|
+
expect(thirdQueryOpts.queryFn).toHaveBeenCalledTimes(1)
|
|
287
|
+
})
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
describe('usePrefetchInfiniteQuery', () => {
|
|
291
|
+
const queryCache = new QueryCache()
|
|
292
|
+
const queryClient = createQueryClient({ queryCache })
|
|
293
|
+
|
|
294
|
+
const Fallback = vi.fn().mockImplementation(() => <div>Loading...</div>)
|
|
295
|
+
|
|
296
|
+
function Suspended<T = unknown>(props: {
|
|
297
|
+
queryOpts: UseInfiniteQueryOptions<
|
|
298
|
+
T,
|
|
299
|
+
Error,
|
|
300
|
+
InfiniteData<T>,
|
|
301
|
+
any,
|
|
302
|
+
Array<string>,
|
|
303
|
+
any
|
|
304
|
+
>
|
|
305
|
+
renderPage: (page: T) => React.JSX.Element
|
|
306
|
+
}) {
|
|
307
|
+
const state = useSuspenseInfiniteQuery(props.queryOpts)
|
|
308
|
+
|
|
309
|
+
return (
|
|
310
|
+
<div>
|
|
311
|
+
{state.data.pages.map((page) => props.renderPage(page))}
|
|
312
|
+
<button onClick={() => state.fetchNextPage()}>Next Page</button>
|
|
313
|
+
</div>
|
|
314
|
+
)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
it('should prefetch an infinite query if query state does not exist', async () => {
|
|
318
|
+
const data = [
|
|
319
|
+
{ data: 'Do you fetch on render?', currentPage: 1, totalPages: 3 },
|
|
320
|
+
{ data: 'Or do you render as you fetch?', currentPage: 2, totalPages: 3 },
|
|
321
|
+
{
|
|
322
|
+
data: 'Either way, Tanstack Query helps you!',
|
|
323
|
+
currentPage: 3,
|
|
324
|
+
totalPages: 3,
|
|
325
|
+
},
|
|
326
|
+
]
|
|
327
|
+
|
|
328
|
+
const queryOpts = {
|
|
329
|
+
queryKey: queryKey(),
|
|
330
|
+
...generateInfiniteQueryOptions(data),
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function App() {
|
|
334
|
+
usePrefetchInfiniteQuery({ ...queryOpts, pages: data.length })
|
|
335
|
+
|
|
336
|
+
return (
|
|
337
|
+
<React.Suspense fallback={<Fallback />}>
|
|
338
|
+
<Suspended
|
|
339
|
+
queryOpts={queryOpts}
|
|
340
|
+
renderPage={(page) => <div>data: {page.data}</div>}
|
|
341
|
+
/>
|
|
342
|
+
</React.Suspense>
|
|
343
|
+
)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const rendered = renderWithClient(queryClient, <App />)
|
|
347
|
+
|
|
348
|
+
await waitFor(() => rendered.getByText('data: Do you fetch on render?'))
|
|
349
|
+
fireEvent.click(rendered.getByText('Next Page'))
|
|
350
|
+
await waitFor(() =>
|
|
351
|
+
rendered.getByText('data: Or do you render as you fetch?'),
|
|
352
|
+
)
|
|
353
|
+
fireEvent.click(rendered.getByText('Next Page'))
|
|
354
|
+
await waitFor(() =>
|
|
355
|
+
rendered.getByText('data: Either way, Tanstack Query helps you!'),
|
|
356
|
+
)
|
|
357
|
+
expect(Fallback).toHaveBeenCalledTimes(1)
|
|
358
|
+
expect(queryOpts.queryFn).toHaveBeenCalledTimes(3)
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
it('should not display fallback if the query cache is already populated', async () => {
|
|
362
|
+
const queryOpts = {
|
|
363
|
+
queryKey: queryKey(),
|
|
364
|
+
...generateInfiniteQueryOptions([
|
|
365
|
+
{ data: 'Prefetch rocks!', currentPage: 1, totalPages: 3 },
|
|
366
|
+
{ data: 'No waterfalls, boy!', currentPage: 2, totalPages: 3 },
|
|
367
|
+
{ data: 'Tanstack Query #ftw', currentPage: 3, totalPages: 3 },
|
|
368
|
+
]),
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
await queryClient.prefetchInfiniteQuery({ ...queryOpts, pages: 3 })
|
|
372
|
+
;(queryOpts.queryFn as Mock).mockClear()
|
|
373
|
+
|
|
374
|
+
function App() {
|
|
375
|
+
usePrefetchInfiniteQuery(queryOpts)
|
|
376
|
+
|
|
377
|
+
return (
|
|
378
|
+
<React.Suspense fallback={<Fallback />}>
|
|
379
|
+
<Suspended
|
|
380
|
+
queryOpts={queryOpts}
|
|
381
|
+
renderPage={(page) => <div>data: {page.data}</div>}
|
|
382
|
+
/>
|
|
383
|
+
</React.Suspense>
|
|
384
|
+
)
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const rendered = renderWithClient(queryClient, <App />)
|
|
388
|
+
|
|
389
|
+
await waitFor(() => rendered.getByText('data: Prefetch rocks!'))
|
|
390
|
+
fireEvent.click(rendered.getByText('Next Page'))
|
|
391
|
+
await waitFor(() => rendered.getByText('data: No waterfalls, boy!'))
|
|
392
|
+
fireEvent.click(rendered.getByText('Next Page'))
|
|
393
|
+
await waitFor(() => rendered.getByText('data: Tanstack Query #ftw'))
|
|
394
|
+
expect(queryOpts.queryFn).not.toHaveBeenCalled()
|
|
395
|
+
expect(Fallback).not.toHaveBeenCalled()
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
it('should not create an endless loop when using inside a suspense boundary', async () => {
|
|
399
|
+
const queryOpts = {
|
|
400
|
+
queryKey: queryKey(),
|
|
401
|
+
...generateInfiniteQueryOptions([
|
|
402
|
+
{ data: 'Infinite Page 1', currentPage: 1, totalPages: 3 },
|
|
403
|
+
{ data: 'Infinite Page 2', currentPage: 1, totalPages: 3 },
|
|
404
|
+
{ data: 'Infinite Page 3', currentPage: 1, totalPages: 3 },
|
|
405
|
+
]),
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function Prefetch({ children }: { children: React.ReactNode }) {
|
|
409
|
+
usePrefetchInfiniteQuery(queryOpts)
|
|
410
|
+
return <>{children}</>
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function App() {
|
|
414
|
+
return (
|
|
415
|
+
<React.Suspense>
|
|
416
|
+
<Prefetch>
|
|
417
|
+
<Suspended
|
|
418
|
+
queryOpts={queryOpts}
|
|
419
|
+
renderPage={(page) => <div>data: {page.data}</div>}
|
|
420
|
+
/>
|
|
421
|
+
</Prefetch>
|
|
422
|
+
</React.Suspense>
|
|
423
|
+
)
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const rendered = renderWithClient(queryClient, <App />)
|
|
427
|
+
await waitFor(() => rendered.getByText('data: Infinite Page 1'))
|
|
428
|
+
fireEvent.click(rendered.getByText('Next Page'))
|
|
429
|
+
await waitFor(() => rendered.getByText('data: Infinite Page 2'))
|
|
430
|
+
fireEvent.click(rendered.getByText('Next Page'))
|
|
431
|
+
await waitFor(() => rendered.getByText('data: Infinite Page 3'))
|
|
432
|
+
expect(queryOpts.queryFn).toHaveBeenCalledTimes(3)
|
|
433
|
+
})
|
|
434
|
+
})
|