Skip to content

Commit a663d10

Browse files
committed
Make querykey optional and provide a default based on method name and request
1 parent 5ac589c commit a663d10

File tree

8 files changed

+117
-19
lines changed

8 files changed

+117
-19
lines changed

README.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,6 @@ const UserProfilePage ({ userID }) => {
7878
const { isLoading, isError, error, data } = useServiceQuery(
7979
UserService.ReadUser,
8080
{ id: userID },
81-
{ queryKey: ['user', userID] },
8281
);
8382
if (isLoading) return <LoadingScreen />;
8483
else if (isError) return <ErrorPage error={error} />;
@@ -112,6 +111,24 @@ mutation.mutate({ id: userID, name: newName });
112111
use suspenses. It is only marginally more convenient than using the service
113112
method directly.
114113

114+
```ts
115+
const reqCtx = useContext(ServiceContext);
116+
const { data: currentWorkspace } = useSuspenseQuery(
117+
queryOptions(UserService.GetUser, { userId }, reqCtx, {
118+
staleTime: 60 * 1000,
119+
}),
120+
);
121+
```
122+
123+
### queryKey
124+
125+
`queryKey` returns a default query key for a service method and request. It is
126+
provided as a convenience and can be overridden in the options param.
127+
128+
```ts
129+
await queryClient.invalidateQueries({ queryKey: queryKey(UserService.ListUsers) });
130+
```
131+
115132
## Contributing
116133

117134
If you find issues or spot possible improvements, please submit a pull-request.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "react-query-grpc-gateway",
3-
"version": "1.2.6",
3+
"version": "1.2.7",
44
"description": "React hook for querying gRPC Gateway endpoints using Tanstack Query.",
55
"type": "module",
66
"main": "dist/index.js",

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export * from './types';
22
export * from './useServiceMutation';
33
export * from './useServiceQuery';
44
export * from './serviceContext';
5+
export * from './queryKey';

src/queryKey.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { ServiceMethod } from './types';
2+
3+
// Returns a default query key for a service method and request object. The key
4+
// structure should not be depended on and considered internal. It is subject to
5+
// change.
6+
export function queryKey<M extends ServiceMethod<Parameters<M>[0], Awaited<ReturnType<M>>>>(
7+
method: M,
8+
req?: Parameters<M>[0],
9+
): [string] | [string, Parameters<M>[0]] {
10+
if (req === undefined) return [method.name];
11+
return [method.name, req];
12+
}

src/types.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
// - O represents the output type for the service method, or the response.
44
// - M represents the service method itself.
55
// - C represents a context.
6+
// - Q represents a QueryKey.
67

7-
import { UseMutationOptions, UseQueryOptions } from '@tanstack/react-query';
8+
import { QueryKey, UseMutationOptions, UseQueryOptions } from '@tanstack/react-query';
89

910
// Represents the static methods from the generated service client.
1011
export type ServiceMethod<I, O> = (req: I, initReq: RequestInitWithPathPrefix) => Promise<O>;
@@ -33,13 +34,16 @@ export interface ErrorResponse {
3334

3435
// Represents the `useServiceQuery` options. This is the same as
3536
// `UseQueryOptions` except that `queryFn` is handled internally, so must not
36-
// be provided. Additionally an `onError` handler can be provided to provide
37-
// customized error handling.
38-
export type UseServiceQueryOptions<M extends ServiceMethod<Parameters<M>[0], ReturnType<M>>> = Omit<
39-
UseQueryOptions<Awaited<ReturnType<M>>, ServiceError>,
40-
'queryFn'
41-
> & {
37+
// be provided and `queryKey` becomes optional.
38+
// Additionally an `onError` handler can be provided to provide customized error
39+
// handling.
40+
export type UseServiceQueryOptions<
41+
M extends ServiceMethod<Parameters<M>[0], ReturnType<M>>,
42+
Q extends QueryKey,
43+
> = Omit<UseQueryOptions<Awaited<ReturnType<M>>, ServiceError>, 'queryFn' | 'queryKey'> & {
4244
onError?: OnErrorHandler<ReturnType<M>>;
45+
} & {
46+
queryKey?: Q;
4347
};
4448

4549
// Represents the `useServiceMutation` options. This is the same as

src/useServiceQuery.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { UseQueryResult, useQuery, UseQueryOptions } from '@tanstack/react-query';
1+
import { UseQueryResult, useQuery, UseQueryOptions, QueryKey } from '@tanstack/react-query';
22
import { useContext } from 'react';
33
import { ServiceContext } from '.';
44
import {
@@ -7,28 +7,36 @@ import {
77
ServiceMethod,
88
UseServiceQueryOptions,
99
} from './types';
10+
import { queryKey } from './queryKey';
1011

1112
// Wraps `useQuery` from react-query, pulling request configuration from context
1213
// and making it easier to call generated service clients.
13-
export function useServiceQuery<M extends ServiceMethod<Parameters<M>[0], Awaited<ReturnType<M>>>>(
14+
export function useServiceQuery<
15+
M extends ServiceMethod<Parameters<M>[0], Awaited<ReturnType<M>>>,
16+
Q extends QueryKey,
17+
>(
1418
method: M,
1519
req: Parameters<M>[0],
16-
options?: UseServiceQueryOptions<M>,
20+
options?: UseServiceQueryOptions<M, Q>,
1721
): UseQueryResult<Awaited<ReturnType<M>>, ServiceError> {
1822
const reqCtx = useContext(ServiceContext);
1923
return useQuery(queryOptions(method, req, reqCtx, options));
2024
}
2125

2226
// Returns the options object for `useQuery` based on the service method. Can
2327
// be used with `useSuspenseQuery` for data loading.
24-
export function queryOptions<M extends ServiceMethod<Parameters<M>[0], Awaited<ReturnType<M>>>>(
28+
export function queryOptions<
29+
M extends ServiceMethod<Parameters<M>[0], Awaited<ReturnType<M>>>,
30+
Q extends QueryKey,
31+
>(
2532
method: M,
2633
req: Parameters<M>[0],
2734
reqInit?: RequestInitWithPathPrefix,
28-
options?: UseServiceQueryOptions<M>,
35+
options?: UseServiceQueryOptions<M, Q>,
2936
): UseQueryOptions<Awaited<ReturnType<M>>, ServiceError> {
3037
return {
31-
...options!,
38+
...options,
39+
queryKey: options?.queryKey ?? queryKey(method, req),
3240
queryFn: () => {
3341
const resp = method(req, {
3442
...reqInit,

tests/queryKey.test.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { queryKey } from '../src/queryKey';
2+
import { RequestInitWithPathPrefix } from '../src/types';
3+
4+
interface FakeRequest {
5+
id: number;
6+
name: string;
7+
}
8+
9+
interface FakeResponse {
10+
req: FakeRequest;
11+
initReq?: RequestInitWithPathPrefix;
12+
}
13+
14+
class FakeService {
15+
static FakeMethod(
16+
this: void,
17+
req: FakeRequest,
18+
initReq?: RequestInitWithPathPrefix,
19+
): Promise<FakeResponse> {
20+
return Promise.resolve({ req, initReq });
21+
}
22+
}
23+
24+
test('query key with request', () => {
25+
const key = queryKey(FakeService.FakeMethod, { id: 1, name: 'Hello' });
26+
expect(key).toEqual(['FakeMethod', { id: 1, name: 'Hello' }]);
27+
});
28+
29+
test('query key with only method', () => {
30+
const key = queryKey(FakeService.FakeMethod);
31+
expect(key).toEqual(['FakeMethod']);
32+
});

tests/useServiceQuery.test.tsx

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,39 @@ class FakeService {
4444
}
4545
}
4646

47-
test('basic method call should return expected data', async () => {
47+
test('basic service call', async () => {
4848
const queryClient = new QueryClient();
4949
const wrapper: FC<PropsWithChildren> = ({ children }) => (
5050
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
5151
);
5252

5353
const { result } = renderHook(
54-
() => useServiceQuery(FakeService.FakeMethod, { id: 1, name: 'Hello' }, { queryKey: ['fake'] }),
54+
() => useServiceQuery(FakeService.FakeMethod, { id: 1, name: 'Hello' }),
55+
{ wrapper },
56+
);
57+
58+
await waitFor(() => expect(result.current.isSuccess).toBe(true));
59+
60+
if (result.current.data) {
61+
expect(result.current.data.req.id).toEqual(1);
62+
expect(result.current.data.req.name).toEqual('Hello');
63+
expect(result.current.data.initReq).toEqual({
64+
headers: { 'Content-Type': 'application/json' },
65+
});
66+
} else {
67+
fail('Expected data to be defined');
68+
}
69+
});
70+
71+
test('service call with custom query key', async () => {
72+
const queryClient = new QueryClient();
73+
const wrapper: FC<PropsWithChildren> = ({ children }) => (
74+
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
75+
);
76+
77+
const { result } = renderHook(
78+
() =>
79+
useServiceQuery(FakeService.FakeMethod, { id: 1, name: 'Hello' }, { queryKey: ['mykey', 1] }),
5580
{ wrapper },
5681
);
5782

@@ -79,7 +104,7 @@ test('service context should override request options', async () => {
79104
);
80105

81106
const { result } = renderHook(
82-
() => useServiceQuery(FakeService.FakeMethod, { id: 1, name: 'Hello' }, { queryKey: ['fake'] }),
107+
() => useServiceQuery(FakeService.FakeMethod, { id: 1, name: 'Hello' }),
83108
{ wrapper },
84109
);
85110

@@ -108,7 +133,6 @@ test('onerror handler should be able to recover from an error', async () => {
108133
FakeService.ErrorMethod,
109134
{ id: 1, name: 'Hello' },
110135
{
111-
queryKey: ['fake'],
112136
onError: (e) => {
113137
if (isErrorResponse(e) && e.code === 16) {
114138
return null;

0 commit comments

Comments
 (0)