Skip to content

Commit 670262f

Browse files
authored
Merge pull request #11 from dev-five-git/react-query
React query
2 parents edfcb41 + 420696f commit 670262f

File tree

18 files changed

+1719
-18
lines changed

18 files changed

+1719
-18
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"changes":{"packages/fetch/package.json":"Patch","packages/react-query/package.json":"Minor"},"note":"Implement react-query","date":"2025-12-02T03:13:38.013730100Z"}

bun.lock

Lines changed: 76 additions & 15 deletions
Large diffs are not rendered by default.

bunfig.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
coverage = true
33
coveragePathIgnorePatterns = ["node_modules", "**/dist/**"]
44
coverageSkipTestFiles = true
5+
preload = ["./packages/react-query/setup.ts"]

package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@
44
"type": "module",
55
"private": true,
66
"devDependencies": {
7-
"@types/bun": "latest",
87
"@biomejs/biome": "^2.3",
9-
"husky": "^9"
8+
"@testing-library/react": "^16.3.0",
9+
"@testing-library/react-hooks": "^8.0.1",
10+
"@types/bun": "latest",
11+
"husky": "^9",
12+
"react": "^19.2.0",
13+
"react-dom": "^19.2.0"
1014
},
1115
"author": "JeongMin Oh",
1216
"license": "Apache-2.0",

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+

packages/react-query/package.json

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"name": "@devup-api/react-query",
3+
"version": "0.0.0",
4+
"license": "Apache-2.0",
5+
"type": "module",
6+
"exports": {
7+
".": {
8+
"import": "./dist/index.js",
9+
"require": "./dist/index.cjs",
10+
"types": "./dist/index.d.ts"
11+
}
12+
},
13+
"files": [
14+
"dist"
15+
],
16+
"scripts": {
17+
"build": "tsc && bun build --target node --outfile=dist/index.js src/index.ts --production --packages=external && bun build --target node --outfile=dist/index.cjs --format=cjs src/index.ts --production --packages=external"
18+
},
19+
"publishConfig": {
20+
"access": "public"
21+
},
22+
"dependencies": {
23+
"@devup-api/fetch": "workspace:*",
24+
"@tanstack/react-query": ">=5.90"
25+
},
26+
"peerDependencies": {
27+
"react": "*",
28+
"@tanstack/react-query": "*"
29+
},
30+
"devDependencies": {
31+
"@testing-library/react-hooks": "^8.0.1",
32+
"@types/node": "^24.10",
33+
"@types/react": "^19.2",
34+
"happy-dom": "^20.0.11",
35+
"typescript": "^5.9"
36+
}
37+
}

packages/react-query/setup.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { beforeAll } from 'bun:test'
2+
3+
// Setup DOM environment for React testing
4+
if (typeof globalThis.document === 'undefined') {
5+
// @ts-expect-error - happy-dom types
6+
const { Window } = await import('happy-dom')
7+
const window = new Window()
8+
const document = window.document
9+
10+
// @ts-expect-error - setting global document
11+
globalThis.window = window
12+
// @ts-expect-error - setting global document
13+
globalThis.document = document
14+
// @ts-expect-error - setting global navigator
15+
globalThis.navigator = window.navigator
16+
// @ts-expect-error - setting global HTMLElement
17+
globalThis.HTMLElement = window.HTMLElement
18+
}
19+
20+
beforeAll(() => {
21+
// Ensure DOM is ready
22+
if (globalThis.document) {
23+
const root = globalThis.document.createElement('div')
24+
root.id = 'root'
25+
globalThis.document.body.appendChild(root)
26+
}
27+
})

0 commit comments

Comments
 (0)