Skip to content

Commit 2cdc35b

Browse files
committed
Implement react query
1 parent edfcb41 commit 2cdc35b

File tree

10 files changed

+1138
-9
lines changed

10 files changed

+1138
-9
lines changed

bun.lock

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -84,15 +84,15 @@
8484
},
8585
"packages/core": {
8686
"name": "@devup-api/core",
87-
"version": "0.1.3",
87+
"version": "0.1.5",
8888
"devDependencies": {
8989
"@types/node": "^24.10",
9090
"typescript": "^5.9",
9191
},
9292
},
9393
"packages/fetch": {
9494
"name": "@devup-api/fetch",
95-
"version": "0.1.3",
95+
"version": "0.1.5",
9696
"dependencies": {
9797
"@devup-api/core": "workspace:*",
9898
},
@@ -103,7 +103,7 @@
103103
},
104104
"packages/generator": {
105105
"name": "@devup-api/generator",
106-
"version": "0.1.3",
106+
"version": "0.1.5",
107107
"dependencies": {
108108
"@devup-api/core": "workspace:*",
109109
"@devup-api/utils": "workspace:*",
@@ -116,7 +116,7 @@
116116
},
117117
"packages/next-plugin": {
118118
"name": "@devup-api/next-plugin",
119-
"version": "0.1.3",
119+
"version": "0.1.5",
120120
"dependencies": {
121121
"@devup-api/core": "workspace:*",
122122
"@devup-api/generator": "workspace:*",
@@ -133,9 +133,26 @@
133133
"next": "*",
134134
},
135135
},
136+
"packages/react-query": {
137+
"name": "@devup-api/react-query",
138+
"version": "0.1.0",
139+
"dependencies": {
140+
"@devup-api/fetch": "workspace:*",
141+
"@tanstack/react-query": ">=5.90",
142+
},
143+
"devDependencies": {
144+
"@types/node": "^24.10",
145+
"@types/react": "^19.2",
146+
"typescript": "^5.9",
147+
},
148+
"peerDependencies": {
149+
"@tanstack/react-query": "*",
150+
"react": "*",
151+
},
152+
},
136153
"packages/rsbuild-plugin": {
137154
"name": "@devup-api/rsbuild-plugin",
138-
"version": "0.1.3",
155+
"version": "0.1.5",
139156
"dependencies": {
140157
"@devup-api/core": "workspace:*",
141158
"@devup-api/generator": "workspace:*",
@@ -152,7 +169,7 @@
152169
},
153170
"packages/utils": {
154171
"name": "@devup-api/utils",
155-
"version": "0.1.3",
172+
"version": "0.1.5",
156173
"devDependencies": {
157174
"@types/node": "^24.10",
158175
"openapi-types": "^12.1",
@@ -161,7 +178,7 @@
161178
},
162179
"packages/vite-plugin": {
163180
"name": "@devup-api/vite-plugin",
164-
"version": "0.1.3",
181+
"version": "0.1.5",
165182
"dependencies": {
166183
"@devup-api/core": "workspace:*",
167184
"@devup-api/generator": "workspace:*",
@@ -178,7 +195,7 @@
178195
},
179196
"packages/webpack-plugin": {
180197
"name": "@devup-api/webpack-plugin",
181-
"version": "0.1.3",
198+
"version": "0.1.5",
182199
"dependencies": {
183200
"@devup-api/core": "workspace:*",
184201
"@devup-api/generator": "workspace:*",
@@ -259,6 +276,8 @@
259276

260277
"@devup-api/next-plugin": ["@devup-api/next-plugin@workspace:packages/next-plugin"],
261278

279+
"@devup-api/react-query": ["@devup-api/react-query@workspace:packages/react-query"],
280+
262281
"@devup-api/rsbuild-plugin": ["@devup-api/rsbuild-plugin@workspace:packages/rsbuild-plugin"],
263282

264283
"@devup-api/utils": ["@devup-api/utils@workspace:packages/utils"],
@@ -513,6 +532,10 @@
513532

514533
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
515534

535+
"@tanstack/query-core": ["@tanstack/query-core@5.90.11", "", {}, "sha512-f9z/nXhCgWDF4lHqgIE30jxLe4sYv15QodfdPDKYAk7nAEjNcndy4dHz3ezhdUaR23BpWa4I2EH4/DZ0//Uf8A=="],
536+
537+
"@tanstack/react-query": ["@tanstack/react-query@5.90.11", "", { "dependencies": { "@tanstack/query-core": "5.90.11" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-3uyzz01D1fkTLXuxF3JfoJoHQMU2fxsfJwE+6N5hHy0dVNoZOvwKP8Z2k7k1KDeD54N20apcJnG75TBAStIrBA=="],
538+
516539
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
517540

518541
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],

packages/fetch/src/__tests__/index.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import * as indexModule from '../index'
55

66
test('index.ts exports', () => {
77
expect({ ...indexModule }).toEqual({
8+
DevupApi: expect.any(Function),
89
createApi: expect.any(Function),
910
})
1011
})

packages/fetch/src/api.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import { getApiEndpointInfo } from './url-map'
2525
import { getApiEndpoint, isPlainObject } from './utils'
2626

2727
// biome-ignore lint/suspicious/noExplicitAny: any is used to allow for flexibility in the type
28-
type DevupApiResponse<T, E = any> =
28+
export type DevupApiResponse<T, E = any> =
2929
| {
3030
data: T
3131
error?: undefined

packages/fetch/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from '@devup-api/core'
2+
export * from './api'
23
export { createApi } from './create-api'

packages/react-query/README.md

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
# @devup-api/react-query
2+
3+
Type-safe React Query hooks built on top of `@devup-api/fetch` and `@tanstack/react-query`.
4+
5+
## Installation
6+
7+
```bash
8+
npm install @devup-api/react-query @tanstack/react-query
9+
```
10+
11+
## Prerequisites
12+
13+
Make sure you have `@tanstack/react-query` set up in your React application:
14+
15+
```tsx
16+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
17+
18+
const queryClient = new QueryClient()
19+
20+
function App() {
21+
return (
22+
<QueryClientProvider client={queryClient}>
23+
{/* Your app */}
24+
</QueryClientProvider>
25+
)
26+
}
27+
```
28+
29+
## Usage
30+
31+
### Create API Hooks Instance
32+
33+
```tsx
34+
import { createApi } from '@devup-api/react-query'
35+
36+
const api = createApi('https://api.example.com', {
37+
headers: {
38+
'Content-Type': 'application/json'
39+
}
40+
})
41+
```
42+
43+
### Using Query Hooks (GET requests)
44+
45+
```tsx
46+
import { createApi } from '@devup-api/react-query'
47+
48+
const api = createApi('https://api.example.com')
49+
50+
function UsersList() {
51+
// Using operationId
52+
const { data, isLoading, error } = api.useGet('getUsers', {
53+
query: { page: 1, limit: 20 }
54+
})
55+
56+
// Using path
57+
const { data: user } = api.useGet('/users/{id}', {
58+
params: { id: '123' },
59+
query: { include: 'posts' }
60+
})
61+
62+
if (isLoading) return <div>Loading...</div>
63+
if (error) return <div>Error: {error.message}</div>
64+
if (data?.error) return <div>API Error: {data.error}</div>
65+
if (data?.data) {
66+
return <div>{/* Render your data */}</div>
67+
}
68+
return null
69+
}
70+
```
71+
72+
### Using Mutation Hooks (POST, PUT, PATCH, DELETE)
73+
74+
#### POST Request
75+
76+
```tsx
77+
function CreateUser() {
78+
const createUser = api.usePost('createUser')
79+
80+
const handleSubmit = () => {
81+
createUser.mutate({
82+
body: {
83+
name: 'John Doe',
84+
email: 'john@example.com'
85+
}
86+
})
87+
}
88+
89+
return (
90+
<div>
91+
<button onClick={handleSubmit} disabled={createUser.isPending}>
92+
{createUser.isPending ? 'Creating...' : 'Create User'}
93+
</button>
94+
{createUser.isError && <div>Error: {createUser.error?.message}</div>}
95+
{createUser.data?.data && <div>Success!</div>}
96+
</div>
97+
)
98+
}
99+
```
100+
101+
#### PUT Request
102+
103+
```tsx
104+
function UpdateUser() {
105+
const updateUser = api.usePut('updateUser')
106+
107+
const handleUpdate = () => {
108+
updateUser.mutate({
109+
params: { id: '123' },
110+
body: {
111+
name: 'Jane Doe'
112+
}
113+
})
114+
}
115+
116+
return <button onClick={handleUpdate}>Update</button>
117+
}
118+
```
119+
120+
#### PATCH Request
121+
122+
```tsx
123+
function PatchUser() {
124+
const patchUser = api.usePatch('patchUser')
125+
126+
const handlePatch = () => {
127+
patchUser.mutate({
128+
params: { id: '123' },
129+
body: {
130+
name: 'Jane Doe'
131+
}
132+
})
133+
}
134+
135+
return <button onClick={handlePatch}>Patch</button>
136+
}
137+
```
138+
139+
#### DELETE Request
140+
141+
```tsx
142+
function DeleteUser() {
143+
const deleteUser = api.useDelete('deleteUser')
144+
145+
const handleDelete = () => {
146+
deleteUser.mutate({
147+
params: { id: '123' }
148+
})
149+
}
150+
151+
return <button onClick={handleDelete}>Delete</button>
152+
}
153+
```
154+
155+
### Advanced Query Options
156+
157+
You can pass additional React Query options to customize behavior:
158+
159+
```tsx
160+
const { data, isLoading } = api.useGet(
161+
'getUsers',
162+
{ query: { page: 1 } },
163+
{
164+
staleTime: 5 * 60 * 1000, // 5 minutes
165+
refetchOnWindowFocus: false,
166+
retry: 3,
167+
}
168+
)
169+
```
170+
171+
### Advanced Mutation Options
172+
173+
You can pass additional React Query mutation options:
174+
175+
```tsx
176+
const createUser = api.usePost('createUser', {
177+
onSuccess: (data) => {
178+
console.log('User created:', data.data)
179+
// Invalidate and refetch users list
180+
queryClient.invalidateQueries({ queryKey: ['getUsers'] })
181+
},
182+
onError: (error) => {
183+
console.error('Failed to create user:', error)
184+
},
185+
})
186+
```
187+
188+
### Creating Hooks from Existing API Instance
189+
190+
If you already have a `DevupApi` instance from `@devup-api/fetch`, you can create hooks from it:
191+
192+
```tsx
193+
import { createApi as createFetchApi } from '@devup-api/fetch'
194+
import { createApiHooks } from '@devup-api/react-query'
195+
196+
const fetchApi = createFetchApi('https://api.example.com')
197+
const api = createApiHooks(fetchApi)
198+
199+
// Now you can use api.useGet, api.usePost, etc.
200+
```
201+
202+
## Response Handling
203+
204+
All hooks return React Query's standard return values, with the response data following the same structure as `@devup-api/fetch`:
205+
206+
```tsx
207+
type DevupApiResponse<T, E> =
208+
| { data: T; error?: undefined; response: Response }
209+
| { data?: undefined; error: E; response: Response }
210+
```
211+
212+
Example:
213+
214+
```tsx
215+
const { data } = api.useGet('getUser', { params: { id: '123' } })
216+
217+
if (data?.data) {
218+
// Success - data.data is fully typed based on your OpenAPI schema
219+
console.log(data.data.name)
220+
console.log(data.data.email)
221+
} else if (data?.error) {
222+
// Error - data.error is typed based on your OpenAPI error schemas
223+
console.error(data.error.message)
224+
}
225+
226+
// Access raw Response object
227+
console.log(data?.response.status)
228+
```
229+
230+
## API Methods
231+
232+
- `api.useGet(path, options, queryOptions)` - GET request hook
233+
- `api.usePost(path, mutationOptions)` - POST request hook
234+
- `api.usePut(path, mutationOptions)` - PUT request hook
235+
- `api.usePatch(path, mutationOptions)` - PATCH request hook
236+
- `api.useDelete(path, mutationOptions)` - DELETE request hook
237+
238+
## Type Safety
239+
240+
All API hooks are fully typed based on your OpenAPI schema:
241+
242+
- Path parameters are type-checked
243+
- Request bodies are type-checked
244+
- Query parameters are type-checked
245+
- Response types are inferred automatically
246+
- Error types are inferred automatically
247+
248+
## License
249+
250+
Apache 2.0
251+

0 commit comments

Comments
 (0)