@richie-rpc/react-query 1.0.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/README.md +360 -0
- package/dist/cjs/index.cjs +74 -0
- package/dist/cjs/index.cjs.map +10 -0
- package/dist/cjs/package.json +5 -0
- package/dist/mjs/index.mjs +47 -0
- package/dist/mjs/index.mjs.map +10 -0
- package/dist/mjs/package.json +5 -0
- package/dist/types/index.d.ts +78 -0
- package/package.json +51 -0
package/README.md
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
# @richie-rpc/react-query
|
|
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.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun add @richie-rpc/react-query @richie-rpc/client @richie-rpc/core @tanstack/react-query zod@^4
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- 🎯 **Fully Type-Safe**: Complete TypeScript inference from contract to hooks
|
|
14
|
+
- 🔄 **Automatic Method Detection**: GET/HEAD → queries, POST/PUT/PATCH/DELETE → mutations
|
|
15
|
+
- âš¡ **React Suspense**: Built-in support with `useSuspenseQuery`
|
|
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
|
|
19
|
+
|
|
20
|
+
## Quick Start
|
|
21
|
+
|
|
22
|
+
### 1. Setup Provider
|
|
23
|
+
|
|
24
|
+
Wrap your app with `QueryClientProvider`:
|
|
25
|
+
|
|
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();
|
|
41
|
+
|
|
42
|
+
function App() {
|
|
43
|
+
return (
|
|
44
|
+
<QueryClientProvider client={queryClient}>
|
|
45
|
+
<YourApp />
|
|
46
|
+
</QueryClientProvider>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### 2. Use Query Hooks (GET requests)
|
|
52
|
+
|
|
53
|
+
Query hooks automatically fetch data when the component mounts:
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
function UserList() {
|
|
57
|
+
const { data, isLoading, error, refetch } = hooks.listUsers.useQuery({
|
|
58
|
+
query: { limit: "10", offset: "0" }
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
if (isLoading) return <div>Loading...</div>;
|
|
62
|
+
if (error) return <div>Error: {error.message}</div>;
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<div>
|
|
66
|
+
{data.data.users.map(user => (
|
|
67
|
+
<div key={user.id}>{user.name}</div>
|
|
68
|
+
))}
|
|
69
|
+
<button onClick={() => refetch()}>Refresh</button>
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### 3. Use Suspense Queries
|
|
76
|
+
|
|
77
|
+
For React Suspense integration:
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
function UserListSuspense() {
|
|
81
|
+
// This will suspend the component until data is loaded
|
|
82
|
+
const { data } = hooks.listUsers.useSuspenseQuery({
|
|
83
|
+
query: { limit: "10" }
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<div>
|
|
88
|
+
{data.data.users.map(user => (
|
|
89
|
+
<div key={user.id}>{user.name}</div>
|
|
90
|
+
))}
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Wrap with Suspense boundary
|
|
96
|
+
function App() {
|
|
97
|
+
return (
|
|
98
|
+
<Suspense fallback={<div>Loading...</div>}>
|
|
99
|
+
<UserListSuspense />
|
|
100
|
+
</Suspense>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### 4. Use Mutation Hooks (POST/PUT/PATCH/DELETE)
|
|
106
|
+
|
|
107
|
+
Mutation hooks don't auto-fetch; they return a function to trigger the request:
|
|
108
|
+
|
|
109
|
+
```tsx
|
|
110
|
+
function CreateUserForm() {
|
|
111
|
+
const mutation = hooks.createUser.useMutation({
|
|
112
|
+
onSuccess: (data) => {
|
|
113
|
+
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);
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
|
|
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
|
+
return (
|
|
134
|
+
<form onSubmit={handleSubmit}>
|
|
135
|
+
<button
|
|
136
|
+
type="submit"
|
|
137
|
+
disabled={mutation.isPending}
|
|
138
|
+
>
|
|
139
|
+
{mutation.isPending ? 'Creating...' : 'Create User'}
|
|
140
|
+
</button>
|
|
141
|
+
{mutation.error && <div>Error: {mutation.error.message}</div>}
|
|
142
|
+
</form>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## API Reference
|
|
148
|
+
|
|
149
|
+
### `createHooks(client, contract)`
|
|
150
|
+
|
|
151
|
+
Creates a typed hooks object from a client and contract.
|
|
152
|
+
|
|
153
|
+
**Parameters:**
|
|
154
|
+
- `client`: Client created with `createClient()`
|
|
155
|
+
- `contract`: Your API contract definition
|
|
156
|
+
|
|
157
|
+
**Returns:** Hooks object with methods for each endpoint
|
|
158
|
+
|
|
159
|
+
### Query Hooks (GET/HEAD methods)
|
|
160
|
+
|
|
161
|
+
#### `hooks.endpointName.useQuery(options, queryOptions?)`
|
|
162
|
+
|
|
163
|
+
Standard query hook for read operations.
|
|
164
|
+
|
|
165
|
+
**Parameters:**
|
|
166
|
+
- `options`: Request options (params, query, headers, body)
|
|
167
|
+
- `queryOptions`: Optional TanStack Query options (staleTime, cacheTime, etc.)
|
|
168
|
+
|
|
169
|
+
**Returns:** `UseQueryResult` with data, isLoading, error, refetch, etc.
|
|
170
|
+
|
|
171
|
+
#### `hooks.endpointName.useSuspenseQuery(options, queryOptions?)`
|
|
172
|
+
|
|
173
|
+
Suspense-enabled query hook.
|
|
174
|
+
|
|
175
|
+
**Parameters:**
|
|
176
|
+
- `options`: Request options (params, query, headers, body)
|
|
177
|
+
- `queryOptions`: Optional TanStack Query options
|
|
178
|
+
|
|
179
|
+
**Returns:** `UseSuspenseQueryResult` with data (always defined when rendered)
|
|
180
|
+
|
|
181
|
+
### Mutation Hooks (POST/PUT/PATCH/DELETE methods)
|
|
182
|
+
|
|
183
|
+
#### `hooks.endpointName.useMutation(mutationOptions?)`
|
|
184
|
+
|
|
185
|
+
Mutation hook for write operations.
|
|
186
|
+
|
|
187
|
+
**Parameters:**
|
|
188
|
+
- `mutationOptions`: Optional TanStack Query mutation options (onSuccess, onError, etc.)
|
|
189
|
+
|
|
190
|
+
**Returns:** `UseMutationResult` with mutate, isPending, error, data, etc.
|
|
191
|
+
|
|
192
|
+
## Advanced Usage
|
|
193
|
+
|
|
194
|
+
### Custom Query Options
|
|
195
|
+
|
|
196
|
+
Pass TanStack Query options for fine-grained control:
|
|
197
|
+
|
|
198
|
+
```tsx
|
|
199
|
+
const { data } = hooks.listUsers.useQuery(
|
|
200
|
+
{ query: { limit: "10" } },
|
|
201
|
+
{
|
|
202
|
+
staleTime: 5 * 60 * 1000, // 5 minutes
|
|
203
|
+
cacheTime: 10 * 60 * 1000, // 10 minutes
|
|
204
|
+
refetchInterval: 30000, // Refetch every 30 seconds
|
|
205
|
+
refetchOnWindowFocus: false,
|
|
206
|
+
}
|
|
207
|
+
);
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Invalidating Queries
|
|
211
|
+
|
|
212
|
+
After a mutation, invalidate related queries to trigger refetch:
|
|
213
|
+
|
|
214
|
+
```tsx
|
|
215
|
+
import { useQueryClient } from '@tanstack/react-query';
|
|
216
|
+
|
|
217
|
+
function DeleteUserButton({ userId }: { userId: string }) {
|
|
218
|
+
const queryClient = useQueryClient();
|
|
219
|
+
|
|
220
|
+
const mutation = hooks.deleteUser.useMutation({
|
|
221
|
+
onSuccess: () => {
|
|
222
|
+
// Invalidate all queries that start with 'listUsers'
|
|
223
|
+
queryClient.invalidateQueries({ queryKey: ['listUsers'] });
|
|
224
|
+
|
|
225
|
+
// Or invalidate specific query
|
|
226
|
+
queryClient.invalidateQueries({
|
|
227
|
+
queryKey: ['getUser', { params: { id: userId } }]
|
|
228
|
+
});
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
return (
|
|
233
|
+
<button onClick={() => mutation.mutate({ params: { id: userId } })}>
|
|
234
|
+
Delete User
|
|
235
|
+
</button>
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Optimistic Updates
|
|
241
|
+
|
|
242
|
+
Update the UI immediately before the server responds:
|
|
243
|
+
|
|
244
|
+
```tsx
|
|
245
|
+
const mutation = hooks.updateUser.useMutation({
|
|
246
|
+
onMutate: async (variables) => {
|
|
247
|
+
// Cancel outgoing refetches
|
|
248
|
+
await queryClient.cancelQueries({ queryKey: ['getUser'] });
|
|
249
|
+
|
|
250
|
+
// Snapshot previous value
|
|
251
|
+
const previousUser = queryClient.getQueryData(['getUser', { params: { id: userId } }]);
|
|
252
|
+
|
|
253
|
+
// Optimistically update
|
|
254
|
+
queryClient.setQueryData(['getUser', { params: { id: userId } }], (old) => ({
|
|
255
|
+
...old,
|
|
256
|
+
data: { ...old.data, ...variables.body }
|
|
257
|
+
}));
|
|
258
|
+
|
|
259
|
+
return { previousUser };
|
|
260
|
+
},
|
|
261
|
+
onError: (err, variables, context) => {
|
|
262
|
+
// Rollback on error
|
|
263
|
+
if (context?.previousUser) {
|
|
264
|
+
queryClient.setQueryData(
|
|
265
|
+
['getUser', { params: { id: userId } }],
|
|
266
|
+
context.previousUser
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
onSettled: () => {
|
|
271
|
+
queryClient.invalidateQueries({ queryKey: ['getUser'] });
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### Dependent Queries
|
|
277
|
+
|
|
278
|
+
Enable queries only when conditions are met:
|
|
279
|
+
|
|
280
|
+
```tsx
|
|
281
|
+
function UserPosts({ userId }: { userId: string | null }) {
|
|
282
|
+
const { data } = hooks.getUserPosts.useQuery(
|
|
283
|
+
{ params: { userId: userId! } },
|
|
284
|
+
{
|
|
285
|
+
enabled: !!userId, // Only fetch when userId is available
|
|
286
|
+
}
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
// ...
|
|
290
|
+
}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Parallel Queries
|
|
294
|
+
|
|
295
|
+
Fetch multiple queries at once:
|
|
296
|
+
|
|
297
|
+
```tsx
|
|
298
|
+
function Dashboard() {
|
|
299
|
+
const users = hooks.listUsers.useQuery({ query: {} });
|
|
300
|
+
const stats = hooks.getStats.useQuery({});
|
|
301
|
+
const settings = hooks.getSettings.useQuery({});
|
|
302
|
+
|
|
303
|
+
if (users.isLoading || stats.isLoading || settings.isLoading) {
|
|
304
|
+
return <div>Loading...</div>;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// All queries are fetched in parallel
|
|
308
|
+
return <div>Dashboard with {users.data.data.total} users</div>;
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
## TypeScript Tips
|
|
313
|
+
|
|
314
|
+
### Inferring Types
|
|
315
|
+
|
|
316
|
+
Extract types from your hooks:
|
|
317
|
+
|
|
318
|
+
```tsx
|
|
319
|
+
import type { EndpointResponse } from '@richie-rpc/client';
|
|
320
|
+
import type { contract } from './contract';
|
|
321
|
+
|
|
322
|
+
// Get the response type for an endpoint
|
|
323
|
+
type UserListResponse = EndpointResponse<typeof contract.listUsers>;
|
|
324
|
+
|
|
325
|
+
// Or extract from hook result
|
|
326
|
+
type UserData = Awaited<ReturnType<typeof hooks.listUsers.useQuery>>['data'];
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### Type-Safe Query Keys
|
|
330
|
+
|
|
331
|
+
Create a helper for consistent query keys:
|
|
332
|
+
|
|
333
|
+
```tsx
|
|
334
|
+
const queryKeys = {
|
|
335
|
+
listUsers: (query: { limit?: string; offset?: string }) =>
|
|
336
|
+
['listUsers', { query }] as const,
|
|
337
|
+
getUser: (id: string) =>
|
|
338
|
+
['getUser', { params: { id } }] as const,
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
// Use in invalidation
|
|
342
|
+
queryClient.invalidateQueries({ queryKey: queryKeys.listUsers({}) });
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
## Best Practices
|
|
346
|
+
|
|
347
|
+
1. **Create hooks once**: Create the hooks object at the module level, not inside components
|
|
348
|
+
2. **Use Suspense for loading states**: Cleaner than manual loading state management
|
|
349
|
+
3. **Invalidate related queries**: After mutations, invalidate queries that may be affected
|
|
350
|
+
4. **Set appropriate staleTime**: Reduce unnecessary refetches by setting staleTime
|
|
351
|
+
5. **Handle errors with Error Boundaries**: Use React Error Boundaries with Suspense queries
|
|
352
|
+
|
|
353
|
+
## Examples
|
|
354
|
+
|
|
355
|
+
See the `packages/demo` directory for complete working examples.
|
|
356
|
+
|
|
357
|
+
## License
|
|
358
|
+
|
|
359
|
+
MIT
|
|
360
|
+
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// @bun @bun-cjs
|
|
2
|
+
(function(exports, require, module, __filename, __dirname) {var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __moduleCache = /* @__PURE__ */ new WeakMap;
|
|
7
|
+
var __toCommonJS = (from) => {
|
|
8
|
+
var entry = __moduleCache.get(from), desc;
|
|
9
|
+
if (entry)
|
|
10
|
+
return entry;
|
|
11
|
+
entry = __defProp({}, "__esModule", { value: true });
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function")
|
|
13
|
+
__getOwnPropNames(from).map((key) => !__hasOwnProp.call(entry, key) && __defProp(entry, key, {
|
|
14
|
+
get: () => from[key],
|
|
15
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
16
|
+
}));
|
|
17
|
+
__moduleCache.set(from, entry);
|
|
18
|
+
return entry;
|
|
19
|
+
};
|
|
20
|
+
var __export = (target, all) => {
|
|
21
|
+
for (var name in all)
|
|
22
|
+
__defProp(target, name, {
|
|
23
|
+
get: all[name],
|
|
24
|
+
enumerable: true,
|
|
25
|
+
configurable: true,
|
|
26
|
+
set: (newValue) => all[name] = () => newValue
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// packages/react-query/index.ts
|
|
31
|
+
var exports_react_query = {};
|
|
32
|
+
__export(exports_react_query, {
|
|
33
|
+
createHooks: () => createHooks
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(exports_react_query);
|
|
36
|
+
var import_react_query = require("@tanstack/react-query");
|
|
37
|
+
function createHooks(client, contract) {
|
|
38
|
+
const hooks = {};
|
|
39
|
+
for (const [name, endpoint] of Object.entries(contract)) {
|
|
40
|
+
const method = endpoint.method;
|
|
41
|
+
const clientMethod = client[name];
|
|
42
|
+
if (method === "GET" || method === "HEAD") {
|
|
43
|
+
hooks[name] = {
|
|
44
|
+
useQuery: (options, queryOptions) => {
|
|
45
|
+
return import_react_query.useQuery({
|
|
46
|
+
queryKey: [name, options],
|
|
47
|
+
queryFn: () => clientMethod(options),
|
|
48
|
+
...queryOptions
|
|
49
|
+
});
|
|
50
|
+
},
|
|
51
|
+
useSuspenseQuery: (options, queryOptions) => {
|
|
52
|
+
return import_react_query.useSuspenseQuery({
|
|
53
|
+
queryKey: [name, options],
|
|
54
|
+
queryFn: () => clientMethod(options),
|
|
55
|
+
...queryOptions
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
} else {
|
|
60
|
+
hooks[name] = {
|
|
61
|
+
useMutation: (mutationOptions) => {
|
|
62
|
+
return import_react_query.useMutation({
|
|
63
|
+
mutationFn: (options) => clientMethod(options),
|
|
64
|
+
...mutationOptions
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return hooks;
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
//# debugId=CC8C1467648847F064756E2164756E21
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../index.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"import type {\n Client,\n ClientMethod,\n EndpointRequestOptions,\n EndpointResponse,\n} from '@richie-rpc/client';\nimport type { Contract, EndpointDefinition } from '@richie-rpc/core';\nimport {\n type UseMutationOptions,\n type UseMutationResult,\n type UseQueryOptions,\n type UseQueryResult,\n type UseSuspenseQueryOptions,\n type UseSuspenseQueryResult,\n useMutation,\n useQuery,\n useSuspenseQuery,\n} from '@tanstack/react-query';\n\n// HTTP methods that should use query hooks (read operations)\ntype QueryMethods = 'GET' | 'HEAD';\n\n// HTTP methods that should use mutation hooks (write operations)\ntype MutationMethods = 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS';\n\n/**\n * Hook wrapper for query endpoints (GET, HEAD)\n * Provides useQuery and useSuspenseQuery methods\n */\nexport type QueryHook<T extends EndpointDefinition> = {\n /**\n * Standard query hook that returns loading states\n */\n useQuery: (\n options: EndpointRequestOptions<T>,\n queryOptions?: Omit<UseQueryOptions<EndpointResponse<T>, Error>, 'queryKey' | 'queryFn'>,\n ) => UseQueryResult<EndpointResponse<T>, Error>;\n\n /**\n * Suspense-enabled query hook that throws promises for React Suspense\n */\n useSuspenseQuery: (\n options: EndpointRequestOptions<T>,\n queryOptions?: Omit<\n UseSuspenseQueryOptions<EndpointResponse<T>, Error>,\n 'queryKey' | 'queryFn'\n >,\n ) => UseSuspenseQueryResult<EndpointResponse<T>, Error>;\n};\n\n/**\n * Hook wrapper for mutation endpoints (POST, PUT, PATCH, DELETE)\n * Provides useMutation method\n */\nexport type MutationHook<T extends EndpointDefinition> = {\n /**\n * Mutation hook for write operations\n */\n useMutation: (\n mutationOptions?: Omit<\n UseMutationOptions<EndpointResponse<T>, Error, EndpointRequestOptions<T>>,\n 'mutationFn'\n >,\n ) => UseMutationResult<EndpointResponse<T>, Error, EndpointRequestOptions<T>>;\n};\n\n/**\n * Conditionally apply hook type based on HTTP method\n */\nexport type EndpointHook<T extends EndpointDefinition> = T['method'] extends QueryMethods\n ? QueryHook<T>\n : T['method'] extends MutationMethods\n ? MutationHook<T>\n : never;\n\n/**\n * Complete hooks object for a contract\n * Each endpoint gets appropriate hooks based on its HTTP method\n */\nexport type Hooks<T extends Contract> = {\n [K in keyof T]: EndpointHook<T[K]>;\n};\n\n/**\n * Create typed React hooks for all endpoints in a contract\n *\n * Query endpoints (GET, HEAD) get useQuery and useSuspenseQuery methods\n * Mutation endpoints (POST, PUT, PATCH, DELETE) get useMutation method\n *\n * @param client - The typed client created with createClient()\n * @param contract - The contract definition\n * @returns Hooks object with methods for each endpoint\n *\n * @example\n * ```tsx\n * const client = createClient(contract, { baseUrl: 'http://localhost:3000' });\n * const hooks = createHooks(client, contract);\n *\n * // In a component - Query\n * function UserList() {\n * const { data, isLoading } = hooks.listUsers.useQuery({\n * query: { limit: \"10\" }\n * });\n * // ...\n * }\n *\n * // In a component - Mutation\n * function CreateUser() {\n * const mutation = hooks.createUser.useMutation();\n * return (\n * <button onClick={() => mutation.mutate({\n * body: { name: \"Alice\", email: \"alice@example.com\" }\n * })}>\n * Create User\n * </button>\n * );\n * }\n * ```\n */\nexport function createHooks<T extends Contract>(client: Client<T>, contract: T): Hooks<T> {\n const hooks: Record<string, unknown> = {};\n\n for (const [name, endpoint] of Object.entries(contract)) {\n const method = endpoint.method;\n const clientMethod = client[name as keyof T] as unknown as ClientMethod<EndpointDefinition>;\n\n if (method === 'GET' || method === 'HEAD') {\n // Create query hooks for read operations\n hooks[name] = {\n useQuery: (\n options: EndpointRequestOptions<EndpointDefinition>,\n queryOptions?: Omit<\n UseQueryOptions<EndpointResponse<EndpointDefinition>, Error>,\n 'queryKey' | 'queryFn'\n >,\n ) => {\n return useQuery({\n queryKey: [name, options],\n queryFn: () => clientMethod(options),\n ...queryOptions,\n });\n },\n useSuspenseQuery: (\n options: EndpointRequestOptions<EndpointDefinition>,\n queryOptions?: Omit<\n UseSuspenseQueryOptions<EndpointResponse<EndpointDefinition>, Error>,\n 'queryKey' | 'queryFn'\n >,\n ) => {\n return useSuspenseQuery({\n queryKey: [name, options],\n queryFn: () => clientMethod(options),\n ...queryOptions,\n });\n },\n };\n } else {\n // Create mutation hooks for write operations\n hooks[name] = {\n useMutation: (\n mutationOptions?: Omit<\n UseMutationOptions<\n EndpointResponse<EndpointDefinition>,\n Error,\n EndpointRequestOptions<EndpointDefinition>\n >,\n 'mutationFn'\n >,\n ) => {\n return useMutation({\n mutationFn: (options: EndpointRequestOptions<EndpointDefinition>) =>\n clientMethod(options),\n ...mutationOptions,\n });\n },\n };\n }\n }\n\n return hooks as Hooks<T>;\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiBO,IAVP;AAgHO,SAAS,WAA+B,CAAC,QAAmB,UAAuB;AAAA,EACxF,MAAM,QAAiC,CAAC;AAAA,EAExC,YAAY,MAAM,aAAa,OAAO,QAAQ,QAAQ,GAAG;AAAA,IACvD,MAAM,SAAS,SAAS;AAAA,IACxB,MAAM,eAAe,OAAO;AAAA,IAE5B,IAAI,WAAW,SAAS,WAAW,QAAQ;AAAA,MAEzC,MAAM,QAAQ;AAAA,QACZ,UAAU,CACR,SACA,iBAIG;AAAA,UACH,OAAO,4BAAS;AAAA,YACd,UAAU,CAAC,MAAM,OAAO;AAAA,YACxB,SAAS,MAAM,aAAa,OAAO;AAAA,eAChC;AAAA,UACL,CAAC;AAAA;AAAA,QAEH,kBAAkB,CAChB,SACA,iBAIG;AAAA,UACH,OAAO,oCAAiB;AAAA,YACtB,UAAU,CAAC,MAAM,OAAO;AAAA,YACxB,SAAS,MAAM,aAAa,OAAO;AAAA,eAChC;AAAA,UACL,CAAC;AAAA;AAAA,MAEL;AAAA,IACF,EAAO;AAAA,MAEL,MAAM,QAAQ;AAAA,QACZ,aAAa,CACX,oBAQG;AAAA,UACH,OAAO,+BAAY;AAAA,YACjB,YAAY,CAAC,YACX,aAAa,OAAO;AAAA,eACnB;AAAA,UACL,CAAC;AAAA;AAAA,MAEL;AAAA;AAAA,EAEJ;AAAA,EAEA,OAAO;AAAA;",
|
|
8
|
+
"debugId": "CC8C1467648847F064756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/react-query/index.ts
|
|
3
|
+
import {
|
|
4
|
+
useMutation,
|
|
5
|
+
useQuery,
|
|
6
|
+
useSuspenseQuery
|
|
7
|
+
} from "@tanstack/react-query";
|
|
8
|
+
function createHooks(client, contract) {
|
|
9
|
+
const hooks = {};
|
|
10
|
+
for (const [name, endpoint] of Object.entries(contract)) {
|
|
11
|
+
const method = endpoint.method;
|
|
12
|
+
const clientMethod = client[name];
|
|
13
|
+
if (method === "GET" || method === "HEAD") {
|
|
14
|
+
hooks[name] = {
|
|
15
|
+
useQuery: (options, queryOptions) => {
|
|
16
|
+
return useQuery({
|
|
17
|
+
queryKey: [name, options],
|
|
18
|
+
queryFn: () => clientMethod(options),
|
|
19
|
+
...queryOptions
|
|
20
|
+
});
|
|
21
|
+
},
|
|
22
|
+
useSuspenseQuery: (options, queryOptions) => {
|
|
23
|
+
return useSuspenseQuery({
|
|
24
|
+
queryKey: [name, options],
|
|
25
|
+
queryFn: () => clientMethod(options),
|
|
26
|
+
...queryOptions
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
} else {
|
|
31
|
+
hooks[name] = {
|
|
32
|
+
useMutation: (mutationOptions) => {
|
|
33
|
+
return useMutation({
|
|
34
|
+
mutationFn: (options) => clientMethod(options),
|
|
35
|
+
...mutationOptions
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return hooks;
|
|
42
|
+
}
|
|
43
|
+
export {
|
|
44
|
+
createHooks
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
//# debugId=579950B8ADA2DE1A64756E2164756E21
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../index.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"import type {\n Client,\n ClientMethod,\n EndpointRequestOptions,\n EndpointResponse,\n} from '@richie-rpc/client';\nimport type { Contract, EndpointDefinition } from '@richie-rpc/core';\nimport {\n type UseMutationOptions,\n type UseMutationResult,\n type UseQueryOptions,\n type UseQueryResult,\n type UseSuspenseQueryOptions,\n type UseSuspenseQueryResult,\n useMutation,\n useQuery,\n useSuspenseQuery,\n} from '@tanstack/react-query';\n\n// HTTP methods that should use query hooks (read operations)\ntype QueryMethods = 'GET' | 'HEAD';\n\n// HTTP methods that should use mutation hooks (write operations)\ntype MutationMethods = 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS';\n\n/**\n * Hook wrapper for query endpoints (GET, HEAD)\n * Provides useQuery and useSuspenseQuery methods\n */\nexport type QueryHook<T extends EndpointDefinition> = {\n /**\n * Standard query hook that returns loading states\n */\n useQuery: (\n options: EndpointRequestOptions<T>,\n queryOptions?: Omit<UseQueryOptions<EndpointResponse<T>, Error>, 'queryKey' | 'queryFn'>,\n ) => UseQueryResult<EndpointResponse<T>, Error>;\n\n /**\n * Suspense-enabled query hook that throws promises for React Suspense\n */\n useSuspenseQuery: (\n options: EndpointRequestOptions<T>,\n queryOptions?: Omit<\n UseSuspenseQueryOptions<EndpointResponse<T>, Error>,\n 'queryKey' | 'queryFn'\n >,\n ) => UseSuspenseQueryResult<EndpointResponse<T>, Error>;\n};\n\n/**\n * Hook wrapper for mutation endpoints (POST, PUT, PATCH, DELETE)\n * Provides useMutation method\n */\nexport type MutationHook<T extends EndpointDefinition> = {\n /**\n * Mutation hook for write operations\n */\n useMutation: (\n mutationOptions?: Omit<\n UseMutationOptions<EndpointResponse<T>, Error, EndpointRequestOptions<T>>,\n 'mutationFn'\n >,\n ) => UseMutationResult<EndpointResponse<T>, Error, EndpointRequestOptions<T>>;\n};\n\n/**\n * Conditionally apply hook type based on HTTP method\n */\nexport type EndpointHook<T extends EndpointDefinition> = T['method'] extends QueryMethods\n ? QueryHook<T>\n : T['method'] extends MutationMethods\n ? MutationHook<T>\n : never;\n\n/**\n * Complete hooks object for a contract\n * Each endpoint gets appropriate hooks based on its HTTP method\n */\nexport type Hooks<T extends Contract> = {\n [K in keyof T]: EndpointHook<T[K]>;\n};\n\n/**\n * Create typed React hooks for all endpoints in a contract\n *\n * Query endpoints (GET, HEAD) get useQuery and useSuspenseQuery methods\n * Mutation endpoints (POST, PUT, PATCH, DELETE) get useMutation method\n *\n * @param client - The typed client created with createClient()\n * @param contract - The contract definition\n * @returns Hooks object with methods for each endpoint\n *\n * @example\n * ```tsx\n * const client = createClient(contract, { baseUrl: 'http://localhost:3000' });\n * const hooks = createHooks(client, contract);\n *\n * // In a component - Query\n * function UserList() {\n * const { data, isLoading } = hooks.listUsers.useQuery({\n * query: { limit: \"10\" }\n * });\n * // ...\n * }\n *\n * // In a component - Mutation\n * function CreateUser() {\n * const mutation = hooks.createUser.useMutation();\n * return (\n * <button onClick={() => mutation.mutate({\n * body: { name: \"Alice\", email: \"alice@example.com\" }\n * })}>\n * Create User\n * </button>\n * );\n * }\n * ```\n */\nexport function createHooks<T extends Contract>(client: Client<T>, contract: T): Hooks<T> {\n const hooks: Record<string, unknown> = {};\n\n for (const [name, endpoint] of Object.entries(contract)) {\n const method = endpoint.method;\n const clientMethod = client[name as keyof T] as unknown as ClientMethod<EndpointDefinition>;\n\n if (method === 'GET' || method === 'HEAD') {\n // Create query hooks for read operations\n hooks[name] = {\n useQuery: (\n options: EndpointRequestOptions<EndpointDefinition>,\n queryOptions?: Omit<\n UseQueryOptions<EndpointResponse<EndpointDefinition>, Error>,\n 'queryKey' | 'queryFn'\n >,\n ) => {\n return useQuery({\n queryKey: [name, options],\n queryFn: () => clientMethod(options),\n ...queryOptions,\n });\n },\n useSuspenseQuery: (\n options: EndpointRequestOptions<EndpointDefinition>,\n queryOptions?: Omit<\n UseSuspenseQueryOptions<EndpointResponse<EndpointDefinition>, Error>,\n 'queryKey' | 'queryFn'\n >,\n ) => {\n return useSuspenseQuery({\n queryKey: [name, options],\n queryFn: () => clientMethod(options),\n ...queryOptions,\n });\n },\n };\n } else {\n // Create mutation hooks for write operations\n hooks[name] = {\n useMutation: (\n mutationOptions?: Omit<\n UseMutationOptions<\n EndpointResponse<EndpointDefinition>,\n Error,\n EndpointRequestOptions<EndpointDefinition>\n >,\n 'mutationFn'\n >,\n ) => {\n return useMutation({\n mutationFn: (options: EndpointRequestOptions<EndpointDefinition>) =>\n clientMethod(options),\n ...mutationOptions,\n });\n },\n };\n }\n }\n\n return hooks as Hooks<T>;\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": ";;AAOA;AAAA;AAAA;AAAA;AAAA;AAgHO,SAAS,WAA+B,CAAC,QAAmB,UAAuB;AAAA,EACxF,MAAM,QAAiC,CAAC;AAAA,EAExC,YAAY,MAAM,aAAa,OAAO,QAAQ,QAAQ,GAAG;AAAA,IACvD,MAAM,SAAS,SAAS;AAAA,IACxB,MAAM,eAAe,OAAO;AAAA,IAE5B,IAAI,WAAW,SAAS,WAAW,QAAQ;AAAA,MAEzC,MAAM,QAAQ;AAAA,QACZ,UAAU,CACR,SACA,iBAIG;AAAA,UACH,OAAO,SAAS;AAAA,YACd,UAAU,CAAC,MAAM,OAAO;AAAA,YACxB,SAAS,MAAM,aAAa,OAAO;AAAA,eAChC;AAAA,UACL,CAAC;AAAA;AAAA,QAEH,kBAAkB,CAChB,SACA,iBAIG;AAAA,UACH,OAAO,iBAAiB;AAAA,YACtB,UAAU,CAAC,MAAM,OAAO;AAAA,YACxB,SAAS,MAAM,aAAa,OAAO;AAAA,eAChC;AAAA,UACL,CAAC;AAAA;AAAA,MAEL;AAAA,IACF,EAAO;AAAA,MAEL,MAAM,QAAQ;AAAA,QACZ,aAAa,CACX,oBAQG;AAAA,UACH,OAAO,YAAY;AAAA,YACjB,YAAY,CAAC,YACX,aAAa,OAAO;AAAA,eACnB;AAAA,UACL,CAAC;AAAA;AAAA,MAEL;AAAA;AAAA,EAEJ;AAAA,EAEA,OAAO;AAAA;",
|
|
8
|
+
"debugId": "579950B8ADA2DE1A64756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { Client, EndpointRequestOptions, EndpointResponse } from '@richie-rpc/client';
|
|
2
|
+
import type { Contract, EndpointDefinition } from '@richie-rpc/core';
|
|
3
|
+
import { type UseMutationOptions, type UseMutationResult, type UseQueryOptions, type UseQueryResult, type UseSuspenseQueryOptions, type UseSuspenseQueryResult } from '@tanstack/react-query';
|
|
4
|
+
type QueryMethods = 'GET' | 'HEAD';
|
|
5
|
+
type MutationMethods = 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS';
|
|
6
|
+
/**
|
|
7
|
+
* Hook wrapper for query endpoints (GET, HEAD)
|
|
8
|
+
* Provides useQuery and useSuspenseQuery methods
|
|
9
|
+
*/
|
|
10
|
+
export type QueryHook<T extends EndpointDefinition> = {
|
|
11
|
+
/**
|
|
12
|
+
* Standard query hook that returns loading states
|
|
13
|
+
*/
|
|
14
|
+
useQuery: (options: EndpointRequestOptions<T>, queryOptions?: Omit<UseQueryOptions<EndpointResponse<T>, Error>, 'queryKey' | 'queryFn'>) => UseQueryResult<EndpointResponse<T>, Error>;
|
|
15
|
+
/**
|
|
16
|
+
* Suspense-enabled query hook that throws promises for React Suspense
|
|
17
|
+
*/
|
|
18
|
+
useSuspenseQuery: (options: EndpointRequestOptions<T>, queryOptions?: Omit<UseSuspenseQueryOptions<EndpointResponse<T>, Error>, 'queryKey' | 'queryFn'>) => UseSuspenseQueryResult<EndpointResponse<T>, Error>;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Hook wrapper for mutation endpoints (POST, PUT, PATCH, DELETE)
|
|
22
|
+
* Provides useMutation method
|
|
23
|
+
*/
|
|
24
|
+
export type MutationHook<T extends EndpointDefinition> = {
|
|
25
|
+
/**
|
|
26
|
+
* Mutation hook for write operations
|
|
27
|
+
*/
|
|
28
|
+
useMutation: (mutationOptions?: Omit<UseMutationOptions<EndpointResponse<T>, Error, EndpointRequestOptions<T>>, 'mutationFn'>) => UseMutationResult<EndpointResponse<T>, Error, EndpointRequestOptions<T>>;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Conditionally apply hook type based on HTTP method
|
|
32
|
+
*/
|
|
33
|
+
export type EndpointHook<T extends EndpointDefinition> = T['method'] extends QueryMethods ? QueryHook<T> : T['method'] extends MutationMethods ? MutationHook<T> : never;
|
|
34
|
+
/**
|
|
35
|
+
* Complete hooks object for a contract
|
|
36
|
+
* Each endpoint gets appropriate hooks based on its HTTP method
|
|
37
|
+
*/
|
|
38
|
+
export type Hooks<T extends Contract> = {
|
|
39
|
+
[K in keyof T]: EndpointHook<T[K]>;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Create typed React hooks for all endpoints in a contract
|
|
43
|
+
*
|
|
44
|
+
* Query endpoints (GET, HEAD) get useQuery and useSuspenseQuery methods
|
|
45
|
+
* Mutation endpoints (POST, PUT, PATCH, DELETE) get useMutation method
|
|
46
|
+
*
|
|
47
|
+
* @param client - The typed client created with createClient()
|
|
48
|
+
* @param contract - The contract definition
|
|
49
|
+
* @returns Hooks object with methods for each endpoint
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```tsx
|
|
53
|
+
* const client = createClient(contract, { baseUrl: 'http://localhost:3000' });
|
|
54
|
+
* const hooks = createHooks(client, contract);
|
|
55
|
+
*
|
|
56
|
+
* // In a component - Query
|
|
57
|
+
* function UserList() {
|
|
58
|
+
* const { data, isLoading } = hooks.listUsers.useQuery({
|
|
59
|
+
* query: { limit: "10" }
|
|
60
|
+
* });
|
|
61
|
+
* // ...
|
|
62
|
+
* }
|
|
63
|
+
*
|
|
64
|
+
* // In a component - Mutation
|
|
65
|
+
* function CreateUser() {
|
|
66
|
+
* const mutation = hooks.createUser.useMutation();
|
|
67
|
+
* return (
|
|
68
|
+
* <button onClick={() => mutation.mutate({
|
|
69
|
+
* body: { name: "Alice", email: "alice@example.com" }
|
|
70
|
+
* })}>
|
|
71
|
+
* Create User
|
|
72
|
+
* </button>
|
|
73
|
+
* );
|
|
74
|
+
* }
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export declare function createHooks<T extends Contract>(client: Client<T>, contract: T): Hooks<T>;
|
|
78
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@richie-rpc/react-query",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"main": "./dist/cjs/index.cjs",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": {
|
|
7
|
+
"types": "./dist/types/index.d.ts",
|
|
8
|
+
"require": "./dist/cjs/index.cjs",
|
|
9
|
+
"import": "./dist/mjs/index.mjs"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"peerDependencies": {
|
|
13
|
+
"@richie-rpc/client": "^1.2.0",
|
|
14
|
+
"@richie-rpc/core": "^1.2.0",
|
|
15
|
+
"@tanstack/react-query": "^5.0.0",
|
|
16
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
17
|
+
"typescript": "^5",
|
|
18
|
+
"zod": "^4.1.12"
|
|
19
|
+
},
|
|
20
|
+
"author": "Richie <oss@ricsam.dev>",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/ricsam/richie-rpc.git"
|
|
25
|
+
},
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/ricsam/richie-rpc/issues"
|
|
28
|
+
},
|
|
29
|
+
"homepage": "https://github.com/ricsam/richie-rpc#readme",
|
|
30
|
+
"keywords": [
|
|
31
|
+
"typescript",
|
|
32
|
+
"bun",
|
|
33
|
+
"zod",
|
|
34
|
+
"api",
|
|
35
|
+
"contract",
|
|
36
|
+
"rpc",
|
|
37
|
+
"rest",
|
|
38
|
+
"openapi",
|
|
39
|
+
"type-safe"
|
|
40
|
+
],
|
|
41
|
+
"description": "A TypeScript-first, type-safe API contract library for Bun with Zod validation",
|
|
42
|
+
"module": "./dist/mjs/index.mjs",
|
|
43
|
+
"types": "./dist/types/index.d.ts",
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
},
|
|
47
|
+
"files": [
|
|
48
|
+
"dist",
|
|
49
|
+
"README.md"
|
|
50
|
+
]
|
|
51
|
+
}
|