@wisemen/vue-core-api-utils 1.2.0 → 2.0.1
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/dist/index.d.mts +254 -281
- package/dist/index.mjs +246 -307
- package/package.json +3 -2
- package/skills/asyncresult-handling/SKILL.md +0 -79
- package/skills/cache-management/SKILL.md +14 -79
- package/skills/foundations/SKILL.md +8 -160
- package/skills/getting-started/SKILL.md +57 -109
- package/skills/optimistic-uis/SKILL.md +52 -273
- package/skills/writing-infinitequeries/SKILL.md +11 -143
- package/skills/writing-mutations/SKILL.md +11 -131
- package/skills/writing-queries/SKILL.md +1 -115
|
@@ -4,19 +4,11 @@ description: >
|
|
|
4
4
|
Infinite pagination with useOffsetInfiniteQuery and useKeysetInfiniteQuery, offset vs keyset strategies determined by backend API, fetchNextPage, hasNextPage, isFetchingNextPage, data/meta result structure, proper page assembly.
|
|
5
5
|
type: core
|
|
6
6
|
library: vue-core-api-utils
|
|
7
|
-
library_version: "0.0.3"
|
|
8
|
-
sources:
|
|
9
|
-
- "wisemen-digital/wisemen-core:docs/packages/api-utils/pages/usage/paginated-query.md"
|
|
10
|
-
- "wisemen-digital/wisemen-core:packages/web/api-utils/src/composables/query/offsetInfiniteQuery.composable.ts"
|
|
11
|
-
- "wisemen-digital/wisemen-core:packages/web/api-utils/src/composables/query/keysetInfiniteQuery.composable.ts"
|
|
12
|
-
subsystems:
|
|
13
|
-
- "Offset Pagination"
|
|
14
|
-
- "Keyset Pagination"
|
|
15
7
|
---
|
|
16
8
|
|
|
17
9
|
# @wisemen/vue-core-api-utils — Writing Infinite Queries
|
|
18
10
|
|
|
19
|
-
Paginate through large datasets with two strategies: offset-based (
|
|
11
|
+
Paginate through large datasets with two strategies: offset-based (offset/limit) for traditional pagination, or keyset-based (cursor/key) for real-time data and large datasets.
|
|
20
12
|
|
|
21
13
|
**Choose your strategy based on what your backend API provides — not preference.**
|
|
22
14
|
|
|
@@ -36,16 +28,16 @@ export function useContactList() {
|
|
|
36
28
|
params: {
|
|
37
29
|
search: computed(() => search.value),
|
|
38
30
|
},
|
|
39
|
-
queryFn: (
|
|
40
|
-
|
|
41
|
-
limit
|
|
31
|
+
queryFn: ({ offset, limit }) => ContactService.getAll({
|
|
32
|
+
offset,
|
|
33
|
+
limit,
|
|
42
34
|
search: search.value,
|
|
43
35
|
}),
|
|
44
36
|
})
|
|
45
37
|
}
|
|
46
38
|
```
|
|
47
39
|
|
|
48
|
-
|
|
40
|
+
The `queryFn` receives `{ offset, limit }` — offset is the starting index (0 on the first page) and limit is the page size.
|
|
49
41
|
|
|
50
42
|
### Keyset Pagination (cursor-based)
|
|
51
43
|
|
|
@@ -61,16 +53,16 @@ export function useContactListKeyset() {
|
|
|
61
53
|
params: {
|
|
62
54
|
search: computed(() => search.value),
|
|
63
55
|
},
|
|
64
|
-
queryFn: (
|
|
65
|
-
limit
|
|
66
|
-
|
|
56
|
+
queryFn: ({ key, limit }) => ContactService.getAllKeyset({
|
|
57
|
+
limit,
|
|
58
|
+
after: key,
|
|
67
59
|
search: search.value,
|
|
68
60
|
}),
|
|
69
61
|
})
|
|
70
62
|
}
|
|
71
63
|
```
|
|
72
64
|
|
|
73
|
-
|
|
65
|
+
The `queryFn` receives `{ key, limit }` — `key` is the cursor (`undefined` on the first page) and `limit` is the page size.
|
|
74
66
|
|
|
75
67
|
## Core Patterns
|
|
76
68
|
|
|
@@ -105,138 +97,14 @@ All pages are automatically concatenated into `data`. Access with `result.getVal
|
|
|
105
97
|
|
|
106
98
|
Use `isFetchingNextPage` (not `isFetching`) to disable the load-more button only during pagination, not during initial load.
|
|
107
99
|
|
|
108
|
-
## Common Mistakes
|
|
109
|
-
|
|
110
|
-
### CRITICAL: Import useInfiniteQuery from @tanstack/vue-query instead of factory
|
|
111
|
-
|
|
112
|
-
```typescript
|
|
113
|
-
// ❌ Wrong: using TanStack directly
|
|
114
|
-
import { useInfiniteQuery } from '@tanstack/vue-query'
|
|
115
|
-
|
|
116
|
-
const { data, error } = useInfiniteQuery({
|
|
117
|
-
queryKey: ['contactList'],
|
|
118
|
-
queryFn: ({ pageParam = 0 }) => ContactService.getAll({ page: pageParam }),
|
|
119
|
-
getNextPageParam: (lastPage) => lastPage.nextPage,
|
|
120
|
-
})
|
|
121
|
-
// Loses AsyncResult, type safety, error codes
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
```typescript
|
|
125
|
-
// ✅ Correct: use factory composable
|
|
126
|
-
import { useOffsetInfiniteQuery } from '@/api'
|
|
127
|
-
|
|
128
|
-
const { result, fetchNextPage, hasNextPage } = useOffsetInfiniteQuery('contactList', {
|
|
129
|
-
params: { search: computed(() => '...') },
|
|
130
|
-
queryFn: (pagination) => ContactService.getAll({
|
|
131
|
-
page: pagination.pageParam,
|
|
132
|
-
limit: pagination.limit,
|
|
133
|
-
}),
|
|
134
|
-
})
|
|
135
|
-
// Full AsyncResult wrapping, type safety, automatic error codes
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
Direct TanStack import loses the factory's type safety, AsyncResult wrapping, and error code typing.
|
|
139
|
-
|
|
140
|
-
Source: Library architecture — always use composables from `createApiUtils()` factory
|
|
141
|
-
|
|
142
|
-
### CRITICAL: Return paginated data without wrapping in data/meta structure
|
|
143
|
-
|
|
144
|
-
```typescript
|
|
145
|
-
// ❌ Wrong: returning array directly
|
|
146
|
-
queryFn: (pagination) => ContactService.getAll({
|
|
147
|
-
page: pagination.pageParam,
|
|
148
|
-
limit: pagination.limit,
|
|
149
|
-
})
|
|
150
|
-
// Returns Contact[] directly instead of { data: Contact[], meta: {...} }
|
|
151
|
-
// QueryClient doesn't know how to append pages; pages overwrite instead of concat
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
```typescript
|
|
155
|
-
// ✅ Correct: return { data, meta } structure
|
|
156
|
-
queryFn: (pagination) => ContactService.getAll({
|
|
157
|
-
page: pagination.pageParam,
|
|
158
|
-
limit: pagination.limit,
|
|
159
|
-
}).then(data => ({
|
|
160
|
-
data,
|
|
161
|
-
meta: {
|
|
162
|
-
page: pagination.pageParam,
|
|
163
|
-
limit: pagination.limit,
|
|
164
|
-
total: 100, // Total count if available
|
|
165
|
-
}
|
|
166
|
-
}))
|
|
167
|
-
// QueryClient knows how to append pages
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
Pagination requires the library to know which part of the response is the data array and which part is pagination metadata. Return an object with `data` (array) and `meta` (metadata).
|
|
171
|
-
|
|
172
|
-
Source: `docs/packages/api-utils/pages/usage/paginated-query.md` Handling Pagination Results
|
|
173
|
-
|
|
174
|
-
### HIGH: Mix offset and keyset pagination patterns in same query
|
|
175
|
-
|
|
176
|
-
```typescript
|
|
177
|
-
// ❌ Wrong: mixing pagination patterns
|
|
178
|
-
const { result } = useOffsetInfiniteQuery('contactList', {
|
|
179
|
-
queryFn: (pagination) => ContactService.getAllKeyset({
|
|
180
|
-
cursor: pagination.pageParam, // offset expects page number!
|
|
181
|
-
limit: pagination.limit,
|
|
182
|
-
}),
|
|
183
|
-
})
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
```typescript
|
|
187
|
-
// ✅ Correct: match composable to backend API
|
|
188
|
-
// Use useOffsetInfiniteQuery for page/limit APIs:
|
|
189
|
-
const { result } = useOffsetInfiniteQuery('contactList', {
|
|
190
|
-
queryFn: (pagination) => ContactService.getAll({
|
|
191
|
-
page: pagination.pageParam,
|
|
192
|
-
limit: pagination.limit,
|
|
193
|
-
}),
|
|
194
|
-
})
|
|
195
|
-
|
|
196
|
-
// Use useKeysetInfiniteQuery for cursor-based APIs:
|
|
197
|
-
const { result } = useKeysetInfiniteQuery('contactList', {
|
|
198
|
-
queryFn: (pagination) => ContactService.getAllKeyset({
|
|
199
|
-
cursor: pagination.pageParam,
|
|
200
|
-
limit: pagination.limit,
|
|
201
|
-
}),
|
|
202
|
-
})
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
Each composable expects a specific pagination parameter type. Offset expects a number; keyset expects a cursor string. Choose the right composable for your backend API.
|
|
206
|
-
|
|
207
|
-
Source: `docs/packages/api-utils/pages/usage/paginated-query.md` Offset vs Keyset comparison
|
|
208
|
-
|
|
209
|
-
### MEDIUM: Forget isFetchingNextPage flag; show loading on first page load
|
|
210
|
-
|
|
211
|
-
```typescript
|
|
212
|
-
// ❌ Wrong: using isFetching on load-more button
|
|
213
|
-
const { result, isFetching, fetchNextPage } = useOffsetInfiniteQuery(...)
|
|
214
|
-
<button @click="fetchNextPage" :disabled="isFetching">
|
|
215
|
-
{{ isFetching ? 'Loading...' : 'Load More' }}
|
|
216
|
-
</button>
|
|
217
|
-
// Button disabled on initial load too!
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
```typescript
|
|
221
|
-
// ✅ Correct: use isFetchingNextPage for pagination button
|
|
222
|
-
const { result, isFetchingNextPage, fetchNextPage } = useOffsetInfiniteQuery(...)
|
|
223
|
-
<button @click="fetchNextPage" :disabled="isFetchingNextPage">
|
|
224
|
-
{{ isFetchingNextPage ? 'Loading...' : 'Load More' }}
|
|
225
|
-
</button>
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
`isFetching` is true during initial load and when fetching next pages. `isFetchingNextPage` is true only when loading additional pages. Use `isFetchingNextPage` for the load-more button.
|
|
229
|
-
|
|
230
|
-
Source: `docs/packages/api-utils/pages/usage/paginated-query.md` Return Values
|
|
231
|
-
|
|
232
100
|
## Backend API Strategy
|
|
233
101
|
|
|
234
102
|
> Offset vs keyset pagination depends entirely on your backend endpoint. Use the strategy your API provides.
|
|
235
103
|
>
|
|
236
104
|
> — Maintainer guidance
|
|
237
105
|
|
|
238
|
-
If your API provides `
|
|
239
|
-
If your API provides a
|
|
106
|
+
If your API provides `offset` and `limit` parameters, use `useOffsetInfiniteQuery`.
|
|
107
|
+
If your API provides a cursor/key parameter, use `useKeysetInfiniteQuery`.
|
|
240
108
|
|
|
241
109
|
## See Also
|
|
242
110
|
|
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: writing-mutations
|
|
3
3
|
description: >
|
|
4
|
-
Create, update, delete resources using
|
|
4
|
+
Create, update, delete resources using useMutation, typed queryKeysToInvalidate, AsyncResult error handling, execute function, request shape with body/params separation.
|
|
5
5
|
type: core
|
|
6
6
|
library: vue-core-api-utils
|
|
7
|
-
library_version: "0.0.3"
|
|
8
|
-
sources:
|
|
9
|
-
- "wisemen-digital/wisemen-core:docs/packages/api-utils/pages/usage/mutation.md"
|
|
10
|
-
- "wisemen-digital/wisemen-core:packages/web/api-utils/src/composables/mutation/mutation.composable.ts"
|
|
11
|
-
- "wisemen-digital/wisemen-core:packages/web/api-utils/src/factory/createApiMutationUtils.ts"
|
|
12
7
|
---
|
|
13
8
|
|
|
14
9
|
# @wisemen/vue-core-api-utils — Writing Mutations
|
|
@@ -23,8 +18,8 @@ import { ContactService } from '@/services'
|
|
|
23
18
|
|
|
24
19
|
export function useCreateContact() {
|
|
25
20
|
return useMutation({
|
|
26
|
-
queryFn: async (
|
|
27
|
-
return await ContactService.create(
|
|
21
|
+
queryFn: async ({ body }: { body: ContactCreateForm }) => {
|
|
22
|
+
return await ContactService.create(body)
|
|
28
23
|
},
|
|
29
24
|
queryKeysToInvalidate: {
|
|
30
25
|
contactList: {}, // Invalidate all contactList queries
|
|
@@ -52,8 +47,7 @@ async function handleSubmit(formData: ContactCreateForm) {
|
|
|
52
47
|
// Invalidated queries will refetch automatically
|
|
53
48
|
} else if (response.isErr()) {
|
|
54
49
|
const error = response.getError()
|
|
55
|
-
|
|
56
|
-
if (error.errors[0].code === 'EMAIL_EXISTS') {
|
|
50
|
+
if ('errors' in error && error.errors[0].code === 'EMAIL_EXISTS') {
|
|
57
51
|
toast.error('That email is already registered')
|
|
58
52
|
} else {
|
|
59
53
|
toast.error('Creation failed')
|
|
@@ -69,12 +63,14 @@ Always `await execute()` and check the result state before continuing.
|
|
|
69
63
|
```typescript
|
|
70
64
|
export function useUpdateContact(contactUuid: string) {
|
|
71
65
|
return useMutation({
|
|
72
|
-
queryFn: async (
|
|
73
|
-
return await ContactService.update(contactUuid,
|
|
66
|
+
queryFn: async ({ body }: { body: ContactUpdateForm }) => {
|
|
67
|
+
return await ContactService.update(contactUuid, body)
|
|
74
68
|
},
|
|
75
69
|
queryKeysToInvalidate: {
|
|
76
|
-
contactDetail: {
|
|
77
|
-
|
|
70
|
+
contactDetail: {
|
|
71
|
+
contactUuid: (_params, _result) => contactUuid, // Invalidate only this contact's detail query
|
|
72
|
+
},
|
|
73
|
+
contactList: {}, // Invalidate all contactList queries
|
|
78
74
|
},
|
|
79
75
|
})
|
|
80
76
|
}
|
|
@@ -109,7 +105,7 @@ async function handleSubmit() {
|
|
|
109
105
|
<button :disabled="result.isLoading()">
|
|
110
106
|
{{ result.isLoading() ? 'Creating...' : 'Create' }}
|
|
111
107
|
</button>
|
|
112
|
-
<div v-if="result.isErr()">
|
|
108
|
+
<div v-if="result.isErr() && 'errors' in result.getError()">
|
|
113
109
|
Error: {{ result.getError().errors[0].detail }}
|
|
114
110
|
</div>
|
|
115
111
|
</form>
|
|
@@ -118,122 +114,6 @@ async function handleSubmit() {
|
|
|
118
114
|
|
|
119
115
|
Use `result.isLoading()` to disable the button during mutation.
|
|
120
116
|
|
|
121
|
-
## Common Mistakes
|
|
122
|
-
|
|
123
|
-
### CRITICAL: Import useMutation from @tanstack/vue-query instead of factory
|
|
124
|
-
|
|
125
|
-
```typescript
|
|
126
|
-
// ❌ Wrong: using TanStack directly
|
|
127
|
-
import { useMutation } from '@tanstack/vue-query'
|
|
128
|
-
|
|
129
|
-
const mutation = useMutation({
|
|
130
|
-
mutationFn: async (data) => ContactService.create(data),
|
|
131
|
-
onSuccess: () => {
|
|
132
|
-
queryClient.invalidateQueries({ queryKey: ['contactList'] })
|
|
133
|
-
},
|
|
134
|
-
})
|
|
135
|
-
// Loses AsyncResult, type safety, error codes
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
```typescript
|
|
139
|
-
// ✅ Correct: use factory composable
|
|
140
|
-
import { useMutation } from '@/api'
|
|
141
|
-
|
|
142
|
-
const { execute, result } = useMutation({
|
|
143
|
-
queryFn: async (options: { body: ContactCreateForm }) => {
|
|
144
|
-
return await ContactService.create(options.body)
|
|
145
|
-
},
|
|
146
|
-
queryKeysToInvalidate: {
|
|
147
|
-
contactList: {}, // Typed, type-safe
|
|
148
|
-
},
|
|
149
|
-
})
|
|
150
|
-
// Full AsyncResult, type-safe queryKeysToInvalidate, error codes
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
Direct TanStack import loses the factory's type safety and AsyncResult wrapping.
|
|
154
|
-
|
|
155
|
-
Source: Library architecture — always use composables from `createApiUtils()` factory
|
|
156
|
-
|
|
157
|
-
### CRITICAL: Forget to list queryKeysToInvalidate; cache becomes stale
|
|
158
|
-
|
|
159
|
-
```typescript
|
|
160
|
-
// ❌ Wrong: no queryKeysToInvalidate
|
|
161
|
-
const { execute } = useMutation({
|
|
162
|
-
queryFn: async (options: { body: ContactCreateForm }) => {
|
|
163
|
-
return await ContactService.create(options.body)
|
|
164
|
-
},
|
|
165
|
-
// Forgot queryKeysToInvalidate!
|
|
166
|
-
})
|
|
167
|
-
// Mutation succeeds but list query still shows old data
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
```typescript
|
|
171
|
-
// ✅ Correct: invalidate affected queries
|
|
172
|
-
const { execute } = useMutation({
|
|
173
|
-
queryFn: async (options: { body: ContactCreateForm }) => {
|
|
174
|
-
return await ContactService.create(options.body)
|
|
175
|
-
},
|
|
176
|
-
queryKeysToInvalidate: {
|
|
177
|
-
contactList: {}, // Invalidate all contactList queries
|
|
178
|
-
},
|
|
179
|
-
})
|
|
180
|
-
// After success, contactList queries refetch
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
If you don't list which queries to invalidate, the cache stays stale and the UI shows outdated data. Update returns success but list still shows old items.
|
|
184
|
-
|
|
185
|
-
Source: `docs/packages/api-utils/pages/usage/mutation.md` Create Mutation Example
|
|
186
|
-
|
|
187
|
-
### HIGH: Not await execute(); code runs before mutation completes
|
|
188
|
-
|
|
189
|
-
```typescript
|
|
190
|
-
// ❌ Wrong: fire and forget
|
|
191
|
-
async function handleSubmit() {
|
|
192
|
-
execute({ body: formData })
|
|
193
|
-
router.push('/contacts') // Redirects before mutation finishes!
|
|
194
|
-
}
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
```typescript
|
|
198
|
-
// ✅ Correct: await the result
|
|
199
|
-
async function handleSubmit() {
|
|
200
|
-
const result = await execute({ body: formData })
|
|
201
|
-
if (result.isOk()) {
|
|
202
|
-
router.push('/contacts')
|
|
203
|
-
}
|
|
204
|
-
// If isErr, form stays visible for retry
|
|
205
|
-
}
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
Not awaiting `execute()` means the mutation is still in flight when you navigate away or access the result. Always await and check the result before continuing.
|
|
209
|
-
|
|
210
|
-
Source: `docs/packages/api-utils/pages/usage/mutation.md` Usage in Component
|
|
211
|
-
|
|
212
|
-
### HIGH: Use body instead of params for query parameters
|
|
213
|
-
|
|
214
|
-
```typescript
|
|
215
|
-
// ❌ Wrong: filter/search as body
|
|
216
|
-
const { execute } = useMutation({
|
|
217
|
-
queryFn: async (options) => {
|
|
218
|
-
return await SearchService.search(options.body) // params should go in URL!
|
|
219
|
-
},
|
|
220
|
-
})
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
```typescript
|
|
224
|
-
// ✅ Correct: separate body and params
|
|
225
|
-
const { execute } = useMutation({
|
|
226
|
-
queryFn: async (options) => {
|
|
227
|
-
const { body, params } = options
|
|
228
|
-
return await SearchService.search(params) // URL params
|
|
229
|
-
},
|
|
230
|
-
})
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
When mutations accept query parameters (filters, search), pass them in the `params` field of the options, not `body`. `body` is for request payload; `params` is for URL query string.
|
|
234
|
-
|
|
235
|
-
Source: Source code `mutation.composable.ts` — request shape documentation
|
|
236
|
-
|
|
237
117
|
## See Also
|
|
238
118
|
|
|
239
119
|
- [Cache Management](../cache-management/SKILL.md) — Understanding which queries to invalidate
|
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: writing-queries
|
|
3
3
|
description: >
|
|
4
|
-
Single resource queries using
|
|
4
|
+
Single resource queries using useQuery, computed ref params, staleTime configuration, queryFn, refetch, isFetching vs isLoading distinctions, automatic cache management.
|
|
5
5
|
type: core
|
|
6
6
|
library: vue-core-api-utils
|
|
7
|
-
library_version: "0.0.3"
|
|
8
|
-
sources:
|
|
9
|
-
- "wisemen-digital/wisemen-core:docs/packages/api-utils/pages/usage/query.md"
|
|
10
|
-
- "wisemen-digital/wisemen-core:docs/packages/api-utils/pages/usage/overview.md"
|
|
11
|
-
- "wisemen-digital/wisemen-core:packages/web/api-utils/src/composables/query/query.composable.ts"
|
|
12
7
|
---
|
|
13
8
|
|
|
14
9
|
# @wisemen/vue-core-api-utils — Writing Queries
|
|
@@ -90,115 +85,6 @@ if (result.value.isOk()) {
|
|
|
90
85
|
}
|
|
91
86
|
```
|
|
92
87
|
|
|
93
|
-
## Common Mistakes
|
|
94
|
-
|
|
95
|
-
### CRITICAL: Import useQuery from @tanstack/vue-query instead of factory
|
|
96
|
-
|
|
97
|
-
```typescript
|
|
98
|
-
// ❌ Wrong: using TanStack directly
|
|
99
|
-
import { useQuery } from '@tanstack/vue-query'
|
|
100
|
-
|
|
101
|
-
const { data, error, isLoading } = useQuery({
|
|
102
|
-
queryKey: ['contactDetail', '123'],
|
|
103
|
-
queryFn: () => ContactService.getByUuid('123'),
|
|
104
|
-
})
|
|
105
|
-
// Loses AsyncResult wrapping, type safety, error code typing
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
```typescript
|
|
109
|
-
// ✅ Correct: use factory-provided composable
|
|
110
|
-
import { useQuery } from '@/api'
|
|
111
|
-
import { computed } from 'vue'
|
|
112
|
-
|
|
113
|
-
const { result, isLoading } = useQuery('contactDetail', {
|
|
114
|
-
params: { contactUuid: computed(() => '123') },
|
|
115
|
-
queryFn: () => ContactService.getByUuid('123'),
|
|
116
|
-
staleTime: 1000 * 60 * 5,
|
|
117
|
-
})
|
|
118
|
-
// Full type safety, AsyncResult wrapping, automatic error codes
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
Importing directly from @tanstack/vue-query bypasses the typed factory, losing AsyncResult wrapping, type-safe query keys, and error code typing.
|
|
122
|
-
|
|
123
|
-
Source: Library architecture — always use composables from `createApiUtils()` factory
|
|
124
|
-
|
|
125
|
-
### HIGH: Use plain ref for params instead of computed
|
|
126
|
-
|
|
127
|
-
```typescript
|
|
128
|
-
// ❌ Wrong: plain ref doesn't trigger refetch
|
|
129
|
-
const userId = ref('123')
|
|
130
|
-
const { result } = useQuery('userDetail', {
|
|
131
|
-
params: { userId }, // plain ref, not computed
|
|
132
|
-
queryFn: () => UserService.getById(userId.value),
|
|
133
|
-
})
|
|
134
|
-
// Later: userId.value = '456'
|
|
135
|
-
// Query does NOT refetch — cache stays stale!
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
```typescript
|
|
139
|
-
// ✅ Correct: use computed so query watches changes
|
|
140
|
-
const userId = computed(() => props.userId)
|
|
141
|
-
const { result } = useQuery('userDetail', {
|
|
142
|
-
params: { userId },
|
|
143
|
-
queryFn: () => UserService.getById(userId.value),
|
|
144
|
-
})
|
|
145
|
-
// userId changes → computed updates → query watches → refetch happens
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
When params are plain refs, the query doesn't watch them and the cache isn't invalidated when the param changes.
|
|
149
|
-
|
|
150
|
-
Source: `docs/packages/api-utils/pages/usage/query.md` Usage in Vue Component section
|
|
151
|
-
|
|
152
|
-
### HIGH: Not set staleTime; serve stale cache indefinitely
|
|
153
|
-
|
|
154
|
-
```typescript
|
|
155
|
-
// ❌ Wrong: no staleTime; background refetch constantly
|
|
156
|
-
const { result } = useQuery('userDetail', {
|
|
157
|
-
params: { userId: computed(() => '123') },
|
|
158
|
-
queryFn: () => UserService.getById('123'),
|
|
159
|
-
// staleTime defaults to 0 — cache is immediately stale!
|
|
160
|
-
})
|
|
161
|
-
// Every component interaction triggers a refetch
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
```typescript
|
|
165
|
-
// ✅ Correct: set staleTime to a reasonable value
|
|
166
|
-
const { result } = useQuery('userDetail', {
|
|
167
|
-
params: { userId: computed(() => '123') },
|
|
168
|
-
queryFn: () => UserService.getById('123'),
|
|
169
|
-
staleTime: 1000 * 60 * 5, // 5 minutes
|
|
170
|
-
})
|
|
171
|
-
// Cache remains fresh for 5 minutes — background refetch only after expiry
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
Default `staleTime` is 0, meaning the cache is immediately considered stale. Every interaction triggers a background refetch. Set `staleTime` based on how frequently the data changes.
|
|
175
|
-
|
|
176
|
-
Source: `docs/packages/api-utils/pages/getting-started/installation.md` Setup section
|
|
177
|
-
|
|
178
|
-
### MEDIUM: Confuse isFetching with isLoading
|
|
179
|
-
|
|
180
|
-
```typescript
|
|
181
|
-
// ❌ Wrong: checking isLoading for conditional render
|
|
182
|
-
const { result, isLoading } = useQuery(...)
|
|
183
|
-
if (isLoading.value) {
|
|
184
|
-
return // Exits only on initial load!
|
|
185
|
-
}
|
|
186
|
-
// Code here runs while background refetch happens
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
```typescript
|
|
190
|
-
// ✅ Correct: use result.isLoading() for state checks
|
|
191
|
-
const { result } = useQuery(...)
|
|
192
|
-
if (result.value.isLoading()) {
|
|
193
|
-
return // True only on initial load
|
|
194
|
-
}
|
|
195
|
-
// Use isFetching separately for background fetch indicator
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
`isLoading` is true only during the initial fetch. `isFetching` is true whenever any fetch is in progress (including background refetches). Use `result.isLoading()` for conditional rendering; use `isFetching` for loading indicators on load-more buttons.
|
|
199
|
-
|
|
200
|
-
Source: `docs/packages/api-utils/pages/usage/query.md` Return Values section
|
|
201
|
-
|
|
202
88
|
## See Also
|
|
203
89
|
|
|
204
90
|
- [Cache Management](../cache-management/SKILL.md) — Understanding caching strategy informs staleTime choices
|