@richie-rpc/react-query 1.0.6 → 1.0.8

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @richie-rpc/react-query
2
2
 
3
- React hooks integration for Richie RPC using TanStack Query (React Query). Provides type-safe hooks with automatic caching, background refetching, and React Suspense support.
3
+ React hooks integration for Richie RPC using TanStack Query (React Query v5). Provides type-safe hooks with automatic caching, background refetching, React Suspense support, and streaming integration.
4
4
 
5
5
  ## Installation
6
6
 
@@ -14,48 +14,31 @@ bun add @richie-rpc/react-query @richie-rpc/client @richie-rpc/core @tanstack/re
14
14
  - 🔄 **Automatic Method Detection**: GET/HEAD → queries, POST/PUT/PATCH/DELETE → mutations
15
15
  - ⚡ **React Suspense**: Built-in support with `useSuspenseQuery`
16
16
  - 💾 **Smart Caching**: Powered by TanStack Query
17
- - 🔥 **Zero Config**: Works out of the box with sensible defaults
18
- - 🎨 **Customizable**: Pass through all TanStack Query options
17
+ - 🎨 **Unified Options**: ts-rest-style `queryKey`/`queryData` pattern
18
+ - 📖 **Infinite Queries**: Built-in pagination support
19
+ - 🌊 **Streaming Integration**: TanStack Query integration via `useStreamQuery`
20
+ - 🔧 **Typed QueryClient**: Per-endpoint cache operations via `createTypedQueryClient`
19
21
 
20
22
  ## Quick Start
21
23
 
22
- ### 1. Setup Provider
23
-
24
- Wrap your app with `QueryClientProvider`:
24
+ ### 1. Create API
25
25
 
26
26
  ```tsx
27
- import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
28
- import { createClient } from '@richie-rpc/client';
29
- import { createHooks } from '@richie-rpc/react-query';
30
- import { contract } from './contract';
31
-
32
- // Create client and hooks
33
- const client = createClient(contract, {
34
- baseUrl: 'http://localhost:3000',
35
- });
36
-
37
- const hooks = createHooks(client, contract);
38
-
39
- // Create query client
40
- const queryClient = new QueryClient();
27
+ import { createTanstackQueryApi } from '@richie-rpc/react-query';
28
+ import { client, contract } from './api'; // your client setup
41
29
 
42
- function App() {
43
- return (
44
- <QueryClientProvider client={queryClient}>
45
- <YourApp />
46
- </QueryClientProvider>
47
- );
48
- }
30
+ const api = createTanstackQueryApi(client, contract);
49
31
  ```
50
32
 
51
33
  ### 2. Use Query Hooks (GET requests)
52
34
 
53
- Query hooks automatically fetch data when the component mounts:
35
+ Query hooks use the unified `queryKey`/`queryData` pattern:
54
36
 
55
37
  ```tsx
56
38
  function UserList() {
57
- const { data, isLoading, error, refetch } = hooks.listUsers.useQuery({
58
- query: { limit: '10', offset: '0' },
39
+ const { data, isLoading, error, refetch } = api.listUsers.useQuery({
40
+ queryKey: ['users', { limit: '10', offset: '0' }],
41
+ queryData: { query: { limit: '10', offset: '0' } },
59
42
  });
60
43
 
61
44
  if (isLoading) return <div>Loading...</div>;
@@ -78,9 +61,9 @@ For React Suspense integration:
78
61
 
79
62
  ```tsx
80
63
  function UserListSuspense() {
81
- // This will suspend the component until data is loaded
82
- const { data } = hooks.listUsers.useSuspenseQuery({
83
- query: { limit: '10' },
64
+ const { data } = api.listUsers.useSuspenseQuery({
65
+ queryKey: ['users'],
66
+ queryData: { query: { limit: '10' } },
84
67
  });
85
68
 
86
69
  return (
@@ -104,38 +87,25 @@ function App() {
104
87
 
105
88
  ### 4. Use Mutation Hooks (POST/PUT/PATCH/DELETE)
106
89
 
107
- Mutation hooks don't auto-fetch; they return a function to trigger the request:
90
+ Mutation hooks return a function to trigger the request:
108
91
 
109
92
  ```tsx
110
93
  function CreateUserForm() {
111
- const mutation = hooks.createUser.useMutation({
94
+ const mutation = api.createUser.useMutation({
112
95
  onSuccess: (data) => {
113
96
  console.log('User created:', data);
114
- // Invalidate and refetch
115
- queryClient.invalidateQueries({ queryKey: ['listUsers'] });
116
- },
117
- onError: (error) => {
118
- console.error('Failed to create user:', error);
97
+ queryClient.invalidateQueries({ queryKey: ['users'] });
119
98
  },
120
99
  });
121
100
 
122
- const handleSubmit = (e: FormEvent) => {
123
- e.preventDefault();
124
- mutation.mutate({
125
- body: {
126
- name: 'Alice',
127
- email: 'alice@example.com',
128
- age: 25,
129
- },
130
- });
131
- };
132
-
133
101
  return (
134
- <form onSubmit={handleSubmit}>
102
+ <form onSubmit={(e) => {
103
+ e.preventDefault();
104
+ mutation.mutate({ body: { name: 'Alice', email: 'alice@example.com' } });
105
+ }}>
135
106
  <button type="submit" disabled={mutation.isPending}>
136
107
  {mutation.isPending ? 'Creating...' : 'Create User'}
137
108
  </button>
138
- {mutation.error && <div>Error: {mutation.error.message}</div>}
139
109
  </form>
140
110
  );
141
111
  }
@@ -143,127 +113,243 @@ function CreateUserForm() {
143
113
 
144
114
  ## API Reference
145
115
 
146
- ### `createHooks(client, contract)`
116
+ ### `createTanstackQueryApi(client, contract)`
147
117
 
148
- Creates a typed hooks object from a client and contract.
118
+ Creates a typed API object from a client and contract.
149
119
 
150
120
  **Parameters:**
151
121
 
152
122
  - `client`: Client created with `createClient()`
153
123
  - `contract`: Your API contract definition
154
124
 
155
- **Returns:** Hooks object with methods for each endpoint
125
+ **Returns:** API object with per-endpoint hooks and methods
156
126
 
157
- ### Query Hooks (GET/HEAD methods)
127
+ ### Query Endpoint API (GET/HEAD)
158
128
 
159
- #### `hooks.endpointName.useQuery(options, queryOptions?)`
129
+ #### `api.endpoint.useQuery(options)`
160
130
 
161
- Standard query hook for read operations.
131
+ Standard query hook.
162
132
 
163
- **Parameters:**
133
+ ```tsx
134
+ const { data, isLoading, error } = api.listUsers.useQuery({
135
+ queryKey: ['users'],
136
+ queryData: { query: { limit: '10' } },
137
+ staleTime: 5000,
138
+ // ...other TanStack Query options
139
+ });
140
+ ```
164
141
 
165
- - `options`: Request options (params, query, headers, body)
166
- - `queryOptions`: Optional TanStack Query options (staleTime, cacheTime, etc.)
142
+ #### `api.endpoint.useSuspenseQuery(options)`
167
143
 
168
- **Returns:** `UseQueryResult` with data, isLoading, error, refetch, etc.
144
+ Suspense-enabled query hook.
169
145
 
170
- #### `hooks.endpointName.useSuspenseQuery(options, queryOptions?)`
146
+ ```tsx
147
+ const { data } = api.listUsers.useSuspenseQuery({
148
+ queryKey: ['users'],
149
+ queryData: { query: { limit: '10' } },
150
+ });
151
+ ```
171
152
 
172
- Suspense-enabled query hook.
153
+ #### `api.endpoint.useInfiniteQuery(options)`
173
154
 
174
- **Parameters:**
155
+ Infinite query for pagination.
175
156
 
176
- - `options`: Request options (params, query, headers, body)
177
- - `queryOptions`: Optional TanStack Query options
157
+ ```tsx
158
+ const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = api.listUsers.useInfiniteQuery({
159
+ queryKey: ['users'],
160
+ queryData: ({ pageParam }) => ({
161
+ query: { limit: '10', offset: String(pageParam) },
162
+ }),
163
+ initialPageParam: 0,
164
+ getNextPageParam: (lastPage, allPages) => {
165
+ const nextOffset = allPages.length * 10;
166
+ return lastPage.data.users.length === 10 ? nextOffset : undefined;
167
+ },
168
+ });
169
+ ```
178
170
 
179
- **Returns:** `UseSuspenseQueryResult` with data (always defined when rendered)
171
+ #### `api.endpoint.useSuspenseInfiniteQuery(options)`
180
172
 
181
- ### Mutation Hooks (POST/PUT/PATCH/DELETE methods)
173
+ Suspense-enabled infinite query.
182
174
 
183
- #### `hooks.endpointName.useMutation(mutationOptions?)`
175
+ #### `api.endpoint.query(options)`
184
176
 
185
- Mutation hook for write operations.
177
+ Direct fetch without React Query.
186
178
 
187
- **Parameters:**
179
+ ```tsx
180
+ const result = await api.listUsers.query({ query: { limit: '10' } });
181
+ ```
188
182
 
189
- - `mutationOptions`: Optional TanStack Query mutation options (onSuccess, onError, etc.)
183
+ ### Mutation Endpoint API (POST/PUT/PATCH/DELETE)
190
184
 
191
- **Returns:** `UseMutationResult` with mutate, isPending, error, data, etc.
185
+ #### `api.endpoint.useMutation(options?)`
192
186
 
193
- ## Advanced Usage
187
+ Mutation hook.
194
188
 
195
- ### Custom Query Options
189
+ ```tsx
190
+ const mutation = api.createUser.useMutation({
191
+ onSuccess: (data) => console.log('Created:', data),
192
+ onError: (error) => console.error('Failed:', error),
193
+ });
194
+
195
+ mutation.mutate({ body: { name: 'Alice', email: 'alice@example.com' } });
196
+ ```
196
197
 
197
- Pass TanStack Query options for fine-grained control:
198
+ #### `api.endpoint.mutate(options)`
199
+
200
+ Direct mutate without React Query.
198
201
 
199
202
  ```tsx
200
- const { data } = hooks.listUsers.useQuery(
201
- { query: { limit: '10' } },
202
- {
203
- staleTime: 5 * 60 * 1000, // 5 minutes
204
- cacheTime: 10 * 60 * 1000, // 10 minutes
205
- refetchInterval: 30000, // Refetch every 30 seconds
206
- refetchOnWindowFocus: false,
207
- },
208
- );
203
+ const result = await api.createUser.mutate({
204
+ body: { name: 'Alice', email: 'alice@example.com' },
205
+ });
209
206
  ```
210
207
 
211
- ### Invalidating Queries
208
+ ### Streaming Endpoint API
212
209
 
213
- After a mutation, invalidate related queries to trigger refetch:
210
+ #### `api.endpoint.stream(options)`
211
+
212
+ Direct stream access with event-based API:
214
213
 
215
214
  ```tsx
216
- import { useQueryClient } from '@tanstack/react-query';
215
+ const result = await api.streamChat.stream({ body: { prompt: 'Hello' } });
217
216
 
218
- function DeleteUserButton({ userId }: { userId: string }) {
219
- const queryClient = useQueryClient();
217
+ result.on('chunk', (chunk) => {
218
+ console.log(chunk.text);
219
+ });
220
220
 
221
- const mutation = hooks.deleteUser.useMutation({
222
- onSuccess: () => {
223
- // Invalidate all queries that start with 'listUsers'
224
- queryClient.invalidateQueries({ queryKey: ['listUsers'] });
221
+ result.on('close', (finalResponse) => {
222
+ console.log('Done:', finalResponse);
223
+ });
224
+ ```
225
225
 
226
- // Or invalidate specific query
227
- queryClient.invalidateQueries({
228
- queryKey: ['getUser', { params: { id: userId } }],
229
- });
230
- },
231
- });
226
+ #### `api.endpoint.useStreamQuery(options)`
232
227
 
233
- return <button onClick={() => mutation.mutate({ params: { id: userId } })}>Delete User</button>;
234
- }
228
+ TanStack Query integration using `experimental_streamedQuery`:
229
+
230
+ ```tsx
231
+ const { data: chunks, isFetching } = api.streamChat.useStreamQuery({
232
+ queryKey: ['chat', prompt],
233
+ queryData: { body: { prompt } },
234
+ refetchMode: 'reset', // 'reset' | 'append' | 'replace'
235
+ });
236
+
237
+ // chunks = accumulated array of chunk objects
238
+ // isFetching = true while streaming
235
239
  ```
236
240
 
237
- ### Optimistic Updates
241
+ ### SSE Endpoint API
238
242
 
239
- Update the UI immediately before the server responds:
243
+ #### `api.endpoint.connect(options)`
244
+
245
+ Direct SSE connection:
240
246
 
241
247
  ```tsx
242
- const mutation = hooks.updateUser.useMutation({
243
- onMutate: async (variables) => {
244
- // Cancel outgoing refetches
245
- await queryClient.cancelQueries({ queryKey: ['getUser'] });
248
+ const connection = api.notifications.connect({ params: { id: '123' } });
246
249
 
247
- // Snapshot previous value
248
- const previousUser = queryClient.getQueryData(['getUser', { params: { id: userId } }]);
250
+ connection.on('message', (data) => {
251
+ console.log('Message:', data.text);
252
+ });
249
253
 
250
- // Optimistically update
251
- queryClient.setQueryData(['getUser', { params: { id: userId } }], (old) => ({
252
- ...old,
253
- data: { ...old.data, ...variables.body },
254
- }));
254
+ connection.on('heartbeat', (data) => {
255
+ console.log('Heartbeat:', data.timestamp);
256
+ });
257
+ ```
255
258
 
256
- return { previousUser };
257
- },
258
- onError: (err, variables, context) => {
259
- // Rollback on error
260
- if (context?.previousUser) {
261
- queryClient.setQueryData(['getUser', { params: { id: userId } }], context.previousUser);
262
- }
263
- },
264
- onSettled: () => {
265
- queryClient.invalidateQueries({ queryKey: ['getUser'] });
266
- },
259
+ ### Download Endpoint API
260
+
261
+ #### `api.endpoint.download(options)`
262
+
263
+ Direct file download:
264
+
265
+ ```tsx
266
+ const response = await api.downloadFile.download({ params: { id: 'file123' } });
267
+ ```
268
+
269
+ ### `createTypedQueryClient(queryClient, client, contract)`
270
+
271
+ Create a typed QueryClient wrapper with per-endpoint cache methods. This is useful for type-safe cache operations like prefetching, getting/setting query data, etc.
272
+
273
+ ```tsx
274
+ import { createTypedQueryClient } from '@richie-rpc/react-query';
275
+
276
+ // Create at module level alongside your api
277
+ const typedClient = createTypedQueryClient(queryClient, client, contract);
278
+
279
+ // Type-safe cache operations
280
+ typedClient.listUsers.getQueryData(['users']);
281
+ typedClient.listUsers.setQueryData(['users'], (old) => ({
282
+ ...old,
283
+ data: { ...old.data, users: [...old.data.users, newUser] },
284
+ }));
285
+
286
+ // Prefetching
287
+ await typedClient.listUsers.prefetchQuery({
288
+ queryKey: ['users'],
289
+ queryData: { query: { limit: '10' } },
290
+ });
291
+ ```
292
+
293
+ **Available methods per query endpoint:**
294
+
295
+ - `getQueryData(queryKey)` - Get cached data
296
+ - `setQueryData(queryKey, updater)` - Update cached data
297
+ - `getQueryState(queryKey)` - Get query state
298
+ - `fetchQuery(options)` - Fetch and cache data
299
+ - `prefetchQuery(options)` - Prefetch data in background
300
+ - `ensureQueryData(options)` - Get cached data or fetch if missing
301
+
302
+ ## Error Handling
303
+
304
+ The package includes error handling utilities:
305
+
306
+ ```tsx
307
+ import { isFetchError, isUnknownErrorResponse } from '@richie-rpc/react-query';
308
+
309
+ const { error, contractEndpoint } = api.getUser.useQuery({
310
+ queryKey: ['user', id],
311
+ queryData: { params: { id } },
312
+ });
313
+
314
+ if (error) {
315
+ if (isFetchError(error)) {
316
+ console.log('Network error:', error.message);
317
+ } else if (isUnknownErrorResponse(error, contractEndpoint)) {
318
+ console.log('Unknown status:', error.status);
319
+ }
320
+ }
321
+ ```
322
+
323
+ ### `isFetchError(error)`
324
+
325
+ Returns `true` if the error is a network/fetch error (not a response).
326
+
327
+ ### `isUnknownErrorResponse(error, endpoint)`
328
+
329
+ Returns `true` if the error is a response with a status code not defined in the contract.
330
+
331
+ ### `isNotKnownResponseError(error, endpoint)`
332
+
333
+ Returns `true` if the error is either a fetch error or an unknown response error.
334
+
335
+ ### `exhaustiveGuard(value)`
336
+
337
+ For compile-time exhaustiveness checking in switch statements.
338
+
339
+ ## Advanced Usage
340
+
341
+ ### Custom Query Options
342
+
343
+ Pass TanStack Query options alongside queryKey and queryData:
344
+
345
+ ```tsx
346
+ const { data } = api.listUsers.useQuery({
347
+ queryKey: ['users'],
348
+ queryData: { query: { limit: '10' } },
349
+ staleTime: 5 * 60 * 1000, // 5 minutes
350
+ gcTime: 10 * 60 * 1000, // 10 minutes (formerly cacheTime)
351
+ refetchInterval: 30000, // Refetch every 30 seconds
352
+ refetchOnWindowFocus: false,
267
353
  });
268
354
  ```
269
355
 
@@ -273,78 +359,100 @@ Enable queries only when conditions are met:
273
359
 
274
360
  ```tsx
275
361
  function UserPosts({ userId }: { userId: string | null }) {
276
- const { data } = hooks.getUserPosts.useQuery(
277
- { params: { userId: userId! } },
278
- {
279
- enabled: !!userId, // Only fetch when userId is available
280
- },
281
- );
282
-
283
- // ...
362
+ const { data } = api.getUserPosts.useQuery({
363
+ queryKey: ['posts', userId],
364
+ queryData: { params: { userId: userId! } },
365
+ enabled: !!userId, // Only fetch when userId is available
366
+ });
284
367
  }
285
368
  ```
286
369
 
287
- ### Parallel Queries
370
+ ### Optimistic Updates
288
371
 
289
- Fetch multiple queries at once:
372
+ Update the UI immediately before the server responds:
290
373
 
291
374
  ```tsx
292
- function Dashboard() {
293
- const users = hooks.listUsers.useQuery({ query: {} });
294
- const stats = hooks.getStats.useQuery({});
295
- const settings = hooks.getSettings.useQuery({});
375
+ // Module level - create once
376
+ const typedClient = createTypedQueryClient(queryClient, client, contract);
296
377
 
297
- if (users.isLoading || stats.isLoading || settings.isLoading) {
298
- return <div>Loading...</div>;
299
- }
378
+ function UpdateUserForm({ userId }: { userId: string }) {
379
+ const mutation = api.updateUser.useMutation({
380
+ onMutate: async (variables) => {
381
+ await queryClient.cancelQueries({ queryKey: ['user', userId] });
382
+ const previousUser = typedClient.getUser.getQueryData(['user', userId]);
300
383
 
301
- // All queries are fetched in parallel
302
- return <div>Dashboard with {users.data.data.total} users</div>;
384
+ typedClient.getUser.setQueryData(['user', userId], (old) => ({
385
+ ...old,
386
+ data: { ...old.data, ...variables.body },
387
+ }));
388
+
389
+ return { previousUser };
390
+ },
391
+ onError: (err, variables, context) => {
392
+ if (context?.previousUser) {
393
+ typedClient.getUser.setQueryData(['user', userId], context.previousUser);
394
+ }
395
+ },
396
+ onSettled: () => {
397
+ queryClient.invalidateQueries({ queryKey: ['user', userId] });
398
+ },
399
+ });
303
400
  }
304
401
  ```
305
402
 
306
- ## TypeScript Tips
403
+ ## TypeScript Types
307
404
 
308
- ### Inferring Types
405
+ ### Exported Types
309
406
 
310
- Extract types from your hooks:
407
+ ```tsx
408
+ import type {
409
+ TsrQueryOptions,
410
+ TsrSuspenseQueryOptions,
411
+ TsrInfiniteQueryOptions,
412
+ TsrSuspenseInfiniteQueryOptions,
413
+ TsrMutationOptions,
414
+ TsrStreamQueryOptions,
415
+ TsrResponse,
416
+ TsrError,
417
+ TypedQueryClient,
418
+ TanstackQueryApi,
419
+ } from '@richie-rpc/react-query';
420
+ ```
421
+
422
+ ### Type Inference
423
+
424
+ Extract types from your API:
311
425
 
312
426
  ```tsx
313
427
  import type { EndpointResponse } from '@richie-rpc/client';
314
- import type { contract } from './contract';
315
428
 
316
429
  // Get the response type for an endpoint
317
430
  type UserListResponse = EndpointResponse<typeof contract.listUsers>;
318
-
319
- // Or extract from hook result
320
- type UserData = Awaited<ReturnType<typeof hooks.listUsers.useQuery>>['data'];
321
431
  ```
322
432
 
323
- ### Type-Safe Query Keys
433
+ ## TanStack Query Re-exports
324
434
 
325
- Create a helper for consistent query keys:
435
+ For version consistency, you can import TanStack Query from this package:
326
436
 
327
437
  ```tsx
328
- const queryKeys = {
329
- listUsers: (query: { limit?: string; offset?: string }) => ['listUsers', { query }] as const,
330
- getUser: (id: string) => ['getUser', { params: { id } }] as const,
331
- };
332
-
333
- // Use in invalidation
334
- queryClient.invalidateQueries({ queryKey: queryKeys.listUsers({}) });
438
+ import { QueryClient, QueryClientProvider } from '@richie-rpc/react-query/tanstack';
335
439
  ```
336
440
 
337
441
  ## Best Practices
338
442
 
339
- 1. **Create hooks once**: Create the hooks object at the module level, not inside components
340
- 2. **Use Suspense for loading states**: Cleaner than manual loading state management
341
- 3. **Invalidate related queries**: After mutations, invalidate queries that may be affected
342
- 4. **Set appropriate staleTime**: Reduce unnecessary refetches by setting staleTime
343
- 5. **Handle errors with Error Boundaries**: Use React Error Boundaries with Suspense queries
443
+ 1. **Create API once**: Create the API object at the module level, not inside components
444
+ 2. **Use meaningful queryKeys**: Include relevant parameters in queryKey for proper cache separation
445
+ 3. **Use Suspense for loading states**: Cleaner than manual loading state management
446
+ 4. **Invalidate related queries**: After mutations, invalidate queries that may be affected
447
+ 5. **Use createTypedQueryClient**: For type-safe cache operations like prefetching and setQueryData
448
+ 6. **Handle errors exhaustively**: Use the error utilities for proper error handling
344
449
 
345
450
  ## Examples
346
451
 
347
- See the `packages/demo` directory for complete working examples.
452
+ See the `packages/demo` directory for complete working examples:
453
+
454
+ - [react-example.tsx](../demo/react-example.tsx) - Query and mutation hooks
455
+ - [dictionary-example.tsx](../demo/dictionary-example.tsx) - Complex data structures
348
456
 
349
457
  ## License
350
458