Skip to content

Commit d9111cc

Browse files
authored
feat(react-query): add skipToken functionality (#683)
1 parent 4e5a9d6 commit d9111cc

5 files changed

Lines changed: 162 additions & 19 deletions

File tree

.changeset/gorgeous-games-sell.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@ts-rest/react-query': minor
3+
---
4+
5+
Add ability to pass `skipToken` to `queryData` similar to how you can pass it to `queryFn` in plain React Query

libs/ts-rest/core/src/lib/type-utils.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,12 +102,22 @@ type OptionalKeys<T> = T extends unknown
102102
}[keyof T]
103103
: never;
104104

105+
// TODO: Replace all usages of this with IfAllPropertiesOptional
105106
export type AreAllPropertiesOptional<T> = T extends Record<string, unknown>
106107
? Exclude<keyof T, OptionalKeys<T>> extends never
107108
? true
108109
: false
109110
: false;
110111

112+
export type IfAllPropertiesOptional<T, TIf, TElse> = T extends Record<
113+
string,
114+
unknown
115+
>
116+
? Exclude<keyof T, OptionalKeys<T>> extends never
117+
? TIf
118+
: TElse
119+
: TElse;
120+
111121
export type OptionalIfAllOptional<
112122
T,
113123
Select extends keyof T = keyof T,

libs/ts-rest/react-query-v5/src/v5/internal/create-hooks.ts

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
usePrefetchQuery,
1313
usePrefetchInfiniteQuery,
1414
QueryOptions,
15+
skipToken,
1516
} from '@tanstack/react-query';
1617
import {
1718
AppRoute,
@@ -70,20 +71,25 @@ function createBaseQueryOptions<
7071
TClientArgs extends ClientArgs,
7172
TOptions extends QueryOptions<any, any>,
7273
>(route: TAppRoute, clientArgs: TClientArgs, options: TOptions): TOptions {
73-
const { queryData, ...rqOptions } = options as unknown as TOptions &
74-
(
75-
| TsRestQueryOptions<TAppRoute, TClientArgs>
76-
| TsRestInfiniteQueryOptions<TAppRoute, TClientArgs>
77-
);
74+
const { queryData: queryDataOrFunction, ...rqOptions } =
75+
options as unknown as TOptions &
76+
(
77+
| TsRestQueryOptions<TAppRoute, TClientArgs>
78+
| TsRestInfiniteQueryOptions<TAppRoute, TClientArgs>
79+
);
7880
return {
7981
...rqOptions,
80-
queryFn: (context?: QueryFunctionContext<QueryKey, unknown>) => {
81-
return apiFetcher(
82-
route,
83-
clientArgs,
84-
context?.signal,
85-
)(typeof queryData === 'function' ? queryData(context!) : queryData);
86-
},
82+
queryFn:
83+
queryDataOrFunction === skipToken
84+
? skipToken
85+
: (context?: QueryFunctionContext<QueryKey, unknown>) => {
86+
const requestData =
87+
typeof queryDataOrFunction === 'function'
88+
? queryDataOrFunction(context!)
89+
: queryDataOrFunction;
90+
91+
return apiFetcher(route, clientArgs, context?.signal)(requestData);
92+
},
8793
} as unknown as TOptions;
8894
}
8995

libs/ts-rest/react-query-v5/src/v5/react-query.spec.tsx

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import '@testing-library/jest-dom';
2-
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
2+
import {
3+
QueryClient,
4+
QueryClientProvider,
5+
skipToken,
6+
} from '@tanstack/react-query';
37
import {
48
waitFor,
59
renderHook,
@@ -235,6 +239,27 @@ describe('react-query', () => {
235239
expect(result.current.data).toStrictEqual(SUCCESS_RESPONSE);
236240
});
237241

242+
it('useQuery should handle skipToken', async () => {
243+
const { result } = renderHook(
244+
() => {
245+
return tsr.health.useQuery({
246+
queryKey: ['health'],
247+
queryData: skipToken,
248+
});
249+
},
250+
{
251+
wrapper: ReactQueryProvider,
252+
},
253+
);
254+
255+
await waitFor(() => {
256+
expect(result.current.isPending).toStrictEqual(true);
257+
});
258+
259+
expect(result.current.data).toStrictEqual(undefined);
260+
expect(api).toHaveBeenCalledTimes(0);
261+
});
262+
238263
it('useQuery should handle error response', async () => {
239264
api.mockResolvedValue({
240265
status: 404,
@@ -799,6 +824,19 @@ describe('react-query', () => {
799824
});
800825
});
801826

827+
it('useSuspenseQuery should not allow skipToken', async () => {
828+
renderHook(
829+
() => {
830+
return tsr.health.useSuspenseQuery({
831+
queryKey: ['health'],
832+
// @ts-expect-error - skipToken is not allowed in suspense
833+
queryData: skipToken,
834+
});
835+
},
836+
{ wrapper: ReactQueryProvider },
837+
);
838+
});
839+
802840
it('should handle mutation', async () => {
803841
api.mockResolvedValue(SUCCESS_RESPONSE);
804842

@@ -937,6 +975,65 @@ describe('react-query', () => {
937975
expect(result.current[1].data).toStrictEqual(SUCCESS_RESPONSE);
938976
});
939977

978+
it('useQueries should handle skipToken', async () => {
979+
api.mockResolvedValue(SUCCESS_RESPONSE);
980+
981+
const { result } = renderHook(
982+
() => {
983+
return tsr.posts.getPost.useQueries({
984+
queries: [
985+
{
986+
queryKey: ['posts', '1'],
987+
queryData: {
988+
params: {
989+
id: '1',
990+
},
991+
},
992+
},
993+
{
994+
queryKey: ['posts', '2'],
995+
queryData: skipToken,
996+
},
997+
],
998+
});
999+
},
1000+
{
1001+
wrapper: ReactQueryProvider,
1002+
},
1003+
);
1004+
1005+
expect(result.current[0].data).toStrictEqual(undefined);
1006+
expect(result.current[0].isPending).toStrictEqual(true);
1007+
1008+
expect(result.current[1].data).toStrictEqual(undefined);
1009+
expect(result.current[1].isPending).toStrictEqual(true);
1010+
1011+
await waitFor(() => {
1012+
expect(result.current[0].isPending).toStrictEqual(false);
1013+
expect(result.current[1].isPending).toStrictEqual(true);
1014+
});
1015+
1016+
expect(result.current[0].data).toStrictEqual(SUCCESS_RESPONSE);
1017+
1018+
expect(result.current[1].data).toStrictEqual(undefined);
1019+
1020+
expect(api).toHaveBeenCalledWith({
1021+
method: 'GET',
1022+
path: 'https://api.com/posts/1',
1023+
body: undefined,
1024+
headers: {
1025+
'x-test': 'test',
1026+
},
1027+
route: contract.posts.getPost,
1028+
signal: expect.any(AbortSignal),
1029+
fetchOptions: {
1030+
signal: expect.any(AbortSignal),
1031+
},
1032+
});
1033+
1034+
expect(api).toHaveBeenCalledTimes(1);
1035+
});
1036+
9401037
it('useQueries should handle `select`', async () => {
9411038
api.mockResolvedValue(SUCCESS_RESPONSE);
9421039

libs/ts-rest/react-query-v5/src/v5/types/hooks-options.ts

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ import {
77
UseQueryOptions as TanStackUseQueryOptions,
88
UseSuspenseInfiniteQueryOptions as TanStackUseSuspenseInfiniteQueryOptions,
99
UseSuspenseQueryOptions as TanStackUseSuspenseQueryOptions,
10+
SkipToken,
1011
} from '@tanstack/react-query';
11-
import { AppRoute, AreAllPropertiesOptional, ClientArgs } from '@ts-rest/core';
12+
import { AppRoute, ClientArgs, IfAllPropertiesOptional } from '@ts-rest/core';
1213
import { QueriesOptions, QueriesResults } from '../internal/queries-options';
1314
import {
1415
SuspenseQueriesOptions,
@@ -20,14 +21,38 @@ export type TsRestQueryOptions<
2021
TAppRoute extends AppRoute,
2122
TClientArgs extends ClientArgs,
2223
TQueryData = RequestData<TAppRoute, TClientArgs>,
23-
> = AreAllPropertiesOptional<TQueryData> extends true
24-
? { queryData?: TQueryData }
25-
: { queryData: TQueryData };
24+
> = IfAllPropertiesOptional<
25+
TQueryData,
26+
{ queryData?: TQueryData | SkipToken },
27+
{ queryData: TQueryData | SkipToken }
28+
>;
29+
30+
export type TsRestSuspenseQueryOptions<
31+
TAppRoute extends AppRoute,
32+
TClientArgs extends ClientArgs,
33+
TQueryData = RequestData<TAppRoute, TClientArgs>,
34+
> = IfAllPropertiesOptional<
35+
TQueryData,
36+
{ queryData?: TQueryData },
37+
{ queryData: TQueryData }
38+
>;
2639

2740
export type TsRestInfiniteQueryOptions<
2841
TAppRoute extends AppRoute,
2942
TClientArgs extends ClientArgs,
3043
TPageParam = unknown,
44+
> = {
45+
queryData:
46+
| ((
47+
context: QueryFunctionContext<QueryKey, TPageParam>,
48+
) => RequestData<TAppRoute, TClientArgs>)
49+
| SkipToken;
50+
};
51+
52+
export type TsRestSuspenseInfiniteQueryOptions<
53+
TAppRoute extends AppRoute,
54+
TClientArgs extends ClientArgs,
55+
TPageParam = unknown,
3156
> = {
3257
queryData: (
3358
context: QueryFunctionContext<QueryKey, TPageParam>,
@@ -96,7 +121,7 @@ export type UseSuspenseQueryOptions<
96121
>,
97122
'queryFn'
98123
> &
99-
TsRestQueryOptions<TAppRoute, TClientArgs>;
124+
TsRestSuspenseQueryOptions<TAppRoute, TClientArgs>;
100125

101126
export type UseInfiniteQueryOptions<
102127
TAppRoute extends AppRoute,
@@ -152,7 +177,7 @@ export type UseSuspenseInfiniteQueryOptions<
152177
>,
153178
'queryFn'
154179
> &
155-
TsRestInfiniteQueryOptions<TAppRoute, TClientArgs, TPageParam>;
180+
TsRestSuspenseInfiniteQueryOptions<TAppRoute, TClientArgs, TPageParam>;
156181

157182
export type UseMutationOptions<
158183
TAppRoute extends AppRoute,

0 commit comments

Comments
 (0)