@wisemen/vue-core-api-utils 2.0.0 → 2.0.2
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/package.json +6 -5
- package/skills/asyncresult-handling/SKILL.md +0 -77
- package/skills/cache-management/SKILL.md +36 -138
- package/skills/foundations/SKILL.md +8 -159
- package/skills/getting-started/SKILL.md +54 -142
- package/skills/optimistic-uis/SKILL.md +58 -222
- package/skills/writing-infinitequeries/SKILL.md +14 -212
- package/skills/writing-mutations/SKILL.md +15 -160
- package/skills/writing-queries/SKILL.md +0 -126
- package/dist/index.d.mts +0 -850
- package/dist/index.mjs +0 -13582
|
@@ -4,25 +4,17 @@ 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: "1.2.0"
|
|
8
|
-
sources:
|
|
9
|
-
- "wisemen-digital/wisemen-core:packages/web/api-utils/src/composables/query/offsetInfiniteQuery.composable.ts"
|
|
10
|
-
- "wisemen-digital/wisemen-core:packages/web/api-utils/src/composables/query/keysetInfiniteQuery.composable.ts"
|
|
11
|
-
- "wisemen-digital/wisemen-core:packages/web/api-utils/src/types/pagination.type.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 (offset/limit) for traditional pagination, or keyset-based (cursor
|
|
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
|
|
|
23
15
|
## Setup
|
|
24
16
|
|
|
25
|
-
### Offset Pagination (
|
|
17
|
+
### Offset Pagination (page-based)
|
|
26
18
|
|
|
27
19
|
```typescript
|
|
28
20
|
import { ref, computed } from 'vue'
|
|
@@ -36,16 +28,16 @@ export function useContactList() {
|
|
|
36
28
|
params: {
|
|
37
29
|
search: computed(() => search.value),
|
|
38
30
|
},
|
|
39
|
-
queryFn: (
|
|
40
|
-
offset
|
|
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
|
-
The `queryFn` receives `{ offset
|
|
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
|
-
The `queryFn` receives `{ key
|
|
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,204 +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
|
-
### Custom page limit
|
|
109
|
-
|
|
110
|
-
```typescript
|
|
111
|
-
useOffsetInfiniteQuery('contactList', {
|
|
112
|
-
params: { search: computed(() => search.value) },
|
|
113
|
-
limit: 50, // Default is 20
|
|
114
|
-
queryFn: (pagination) => ContactService.getAll({
|
|
115
|
-
offset: pagination.offset,
|
|
116
|
-
limit: pagination.limit,
|
|
117
|
-
}),
|
|
118
|
-
})
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
Pass `limit` as a top-level option to override the default page size (20).
|
|
122
|
-
|
|
123
|
-
## Response Structures
|
|
124
|
-
|
|
125
|
-
### Offset pagination response
|
|
126
|
-
|
|
127
|
-
Your `queryFn` must return:
|
|
128
|
-
```typescript
|
|
129
|
-
{
|
|
130
|
-
data: Contact[],
|
|
131
|
-
meta: {
|
|
132
|
-
offset: number, // Current offset
|
|
133
|
-
limit: number, // Items per page
|
|
134
|
-
total: number, // Total items across all pages
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
The library uses `meta.offset + meta.limit >= meta.total` to determine if there are more pages.
|
|
140
|
-
|
|
141
|
-
### Keyset pagination response
|
|
142
|
-
|
|
143
|
-
Your `queryFn` must return:
|
|
144
|
-
```typescript
|
|
145
|
-
{
|
|
146
|
-
data: Contact[],
|
|
147
|
-
meta: {
|
|
148
|
-
next: unknown, // Cursor for the next page; null/undefined if no more pages
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
The library uses `meta.next` as the `key` parameter for the subsequent page fetch.
|
|
154
|
-
|
|
155
|
-
## Common Mistakes
|
|
156
|
-
|
|
157
|
-
### CRITICAL: Import useInfiniteQuery from @tanstack/vue-query instead of your api module
|
|
158
|
-
|
|
159
|
-
```typescript
|
|
160
|
-
// ❌ Wrong: using TanStack directly
|
|
161
|
-
import { useInfiniteQuery } from '@tanstack/vue-query'
|
|
162
|
-
|
|
163
|
-
const { data, error } = useInfiniteQuery({
|
|
164
|
-
queryKey: ['contactList'],
|
|
165
|
-
queryFn: ({ pageParam = 0 }) => ContactService.getAll({ page: pageParam }),
|
|
166
|
-
getNextPageParam: (lastPage) => lastPage.nextPage,
|
|
167
|
-
})
|
|
168
|
-
// Loses AsyncResult, type safety, error codes
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
```typescript
|
|
172
|
-
// ✅ Correct: use the composable from your api module
|
|
173
|
-
import { useOffsetInfiniteQuery } from '@/api'
|
|
174
|
-
|
|
175
|
-
const { result, fetchNextPage, hasNextPage } = useOffsetInfiniteQuery('contactList', {
|
|
176
|
-
params: { search: computed(() => '...') },
|
|
177
|
-
queryFn: (pagination) => ContactService.getAll({
|
|
178
|
-
offset: pagination.offset,
|
|
179
|
-
limit: pagination.limit,
|
|
180
|
-
}),
|
|
181
|
-
})
|
|
182
|
-
// Full AsyncResult wrapping, type safety, automatic error codes
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
Source: `src/composables/query/offsetInfiniteQuery.composable.ts`
|
|
186
|
-
|
|
187
|
-
### CRITICAL: Return paginated data without wrapping in data/meta structure
|
|
188
|
-
|
|
189
|
-
```typescript
|
|
190
|
-
// ❌ Wrong: returning array directly
|
|
191
|
-
queryFn: (pagination) => ContactService.getAll({
|
|
192
|
-
offset: pagination.offset,
|
|
193
|
-
limit: pagination.limit,
|
|
194
|
-
})
|
|
195
|
-
// Returns Contact[] directly instead of { data: Contact[], meta: { offset, limit, total } }
|
|
196
|
-
// Library can't determine if there are more pages — infinite loop or stops too early
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
```typescript
|
|
200
|
-
// ✅ Correct: return { data, meta } structure
|
|
201
|
-
queryFn: (pagination) => ContactService.getAll({
|
|
202
|
-
offset: pagination.offset,
|
|
203
|
-
limit: pagination.limit,
|
|
204
|
-
})
|
|
205
|
-
// Where ContactService.getAll already returns { data: Contact[], meta: { offset, limit, total } }
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
The library requires the `{ data, meta }` shape to know how to concatenate pages and when to stop.
|
|
209
|
-
|
|
210
|
-
Source: `src/types/pagination.type.ts` — `OffsetPaginationResponse`
|
|
211
|
-
|
|
212
|
-
### HIGH: Use pageParam or cursor instead of offset/key
|
|
213
|
-
|
|
214
|
-
```typescript
|
|
215
|
-
// ❌ Wrong: using old pageParam naming
|
|
216
|
-
queryFn: (pagination) => ContactService.getAll({
|
|
217
|
-
page: pagination.pageParam, // pageParam doesn't exist!
|
|
218
|
-
cursor: pagination.cursor, // cursor doesn't exist!
|
|
219
|
-
})
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
```typescript
|
|
223
|
-
// ✅ Correct: use offset for offset pagination, key for keyset
|
|
224
|
-
// Offset:
|
|
225
|
-
queryFn: (pagination) => ContactService.getAll({
|
|
226
|
-
offset: pagination.offset, // OffsetPaginationParams.offset
|
|
227
|
-
limit: pagination.limit,
|
|
228
|
-
})
|
|
229
|
-
|
|
230
|
-
// Keyset:
|
|
231
|
-
queryFn: (pagination) => ContactService.getAllKeyset({
|
|
232
|
-
key: pagination.key, // KeysetPaginationParams.key
|
|
233
|
-
limit: pagination.limit,
|
|
234
|
-
})
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
`OffsetPaginationParams` has `{ offset: number, limit: number }`.
|
|
238
|
-
`KeysetPaginationParams` has `{ key?: any, limit: number }`.
|
|
239
|
-
|
|
240
|
-
Source: `src/types/pagination.type.ts`
|
|
241
|
-
|
|
242
|
-
### HIGH: Mix offset and keyset pagination patterns in same query
|
|
243
|
-
|
|
244
|
-
```typescript
|
|
245
|
-
// ❌ Wrong: mixing pagination patterns
|
|
246
|
-
const { result } = useOffsetInfiniteQuery('contactList', {
|
|
247
|
-
queryFn: (pagination) => ContactService.getAllKeyset({
|
|
248
|
-
key: pagination.key, // offset composable doesn't have key!
|
|
249
|
-
limit: pagination.limit,
|
|
250
|
-
}),
|
|
251
|
-
})
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
```typescript
|
|
255
|
-
// ✅ Correct: match composable to backend API
|
|
256
|
-
// Use useOffsetInfiniteQuery for offset/limit APIs:
|
|
257
|
-
const { result } = useOffsetInfiniteQuery('contactList', {
|
|
258
|
-
queryFn: (pagination) => ContactService.getAll({
|
|
259
|
-
offset: pagination.offset,
|
|
260
|
-
limit: pagination.limit,
|
|
261
|
-
}),
|
|
262
|
-
})
|
|
263
|
-
|
|
264
|
-
// Use useKeysetInfiniteQuery for cursor-based APIs:
|
|
265
|
-
const { result } = useKeysetInfiniteQuery('contactListKeyset', {
|
|
266
|
-
queryFn: (pagination) => ContactService.getAllKeyset({
|
|
267
|
-
key: pagination.key,
|
|
268
|
-
limit: pagination.limit,
|
|
269
|
-
}),
|
|
270
|
-
})
|
|
271
|
-
```
|
|
272
|
-
|
|
273
|
-
Each composable expects a specific pagination parameter type. Choose the right composable for your backend API.
|
|
274
|
-
|
|
275
|
-
Source: `src/composables/query/offsetInfiniteQuery.composable.ts` and `keysetInfiniteQuery.composable.ts`
|
|
276
|
-
|
|
277
|
-
### MEDIUM: Forget isFetchingNextPage flag; show loading on first page load
|
|
278
|
-
|
|
279
|
-
```typescript
|
|
280
|
-
// ❌ Wrong: using isFetching on load-more button
|
|
281
|
-
const { result, isFetching, fetchNextPage } = useOffsetInfiniteQuery(...)
|
|
282
|
-
<button @click="fetchNextPage" :disabled="isFetching">
|
|
283
|
-
{{ isFetching ? 'Loading...' : 'Load More' }}
|
|
284
|
-
</button>
|
|
285
|
-
// Button disabled on initial load too!
|
|
286
|
-
```
|
|
287
|
-
|
|
288
|
-
```typescript
|
|
289
|
-
// ✅ Correct: use isFetchingNextPage for pagination button
|
|
290
|
-
const { result, isFetchingNextPage, fetchNextPage } = useOffsetInfiniteQuery(...)
|
|
291
|
-
<button @click="fetchNextPage" :disabled="isFetchingNextPage">
|
|
292
|
-
{{ isFetchingNextPage ? 'Loading...' : 'Load More' }}
|
|
293
|
-
</button>
|
|
294
|
-
```
|
|
295
|
-
|
|
296
|
-
`isFetching` is true during initial load and when fetching next pages. `isFetchingNextPage` is true only when loading additional pages.
|
|
297
|
-
|
|
298
|
-
Source: `src/composables/query/offsetInfiniteQuery.composable.ts` — `UseOffsetInfiniteQueryReturnType`
|
|
299
|
-
|
|
300
100
|
## Backend API Strategy
|
|
301
101
|
|
|
302
102
|
> Offset vs keyset pagination depends entirely on your backend endpoint. Use the strategy your API provides.
|
|
103
|
+
>
|
|
104
|
+
> — Maintainer guidance
|
|
303
105
|
|
|
304
|
-
If your API
|
|
305
|
-
If your API
|
|
106
|
+
If your API provides `offset` and `limit` parameters, use `useOffsetInfiniteQuery`.
|
|
107
|
+
If your API provides a cursor/key parameter, use `useKeysetInfiniteQuery`.
|
|
306
108
|
|
|
307
109
|
## See Also
|
|
308
110
|
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: writing-mutations
|
|
3
3
|
description: >
|
|
4
|
-
Create, update, delete resources using useMutation, typed queryKeysToInvalidate
|
|
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: "1.2.0"
|
|
8
|
-
sources:
|
|
9
|
-
- "wisemen-digital/wisemen-core:packages/web/api-utils/src/composables/mutation/mutation.composable.ts"
|
|
10
7
|
---
|
|
11
8
|
|
|
12
9
|
# @wisemen/vue-core-api-utils — Writing Mutations
|
|
@@ -21,8 +18,8 @@ import { ContactService } from '@/services'
|
|
|
21
18
|
|
|
22
19
|
export function useCreateContact() {
|
|
23
20
|
return useMutation({
|
|
24
|
-
queryFn: async (
|
|
25
|
-
return await ContactService.create(
|
|
21
|
+
queryFn: async ({ body }: { body: ContactCreateForm }) => {
|
|
22
|
+
return await ContactService.create(body)
|
|
26
23
|
},
|
|
27
24
|
queryKeysToInvalidate: {
|
|
28
25
|
contactList: {}, // Invalidate all contactList queries
|
|
@@ -31,7 +28,7 @@ export function useCreateContact() {
|
|
|
31
28
|
}
|
|
32
29
|
```
|
|
33
30
|
|
|
34
|
-
Every mutation
|
|
31
|
+
Every mutation must list which queries to invalidate via `queryKeysToInvalidate`.
|
|
35
32
|
|
|
36
33
|
## Core Patterns
|
|
37
34
|
|
|
@@ -61,54 +58,31 @@ async function handleSubmit(formData: ContactCreateForm) {
|
|
|
61
58
|
|
|
62
59
|
Always `await execute()` and check the result state before continuing.
|
|
63
60
|
|
|
64
|
-
### Update mutation with specific query invalidation
|
|
65
|
-
|
|
66
|
-
When you need to invalidate a specific query (rather than all queries with a key), pass param extractor functions:
|
|
61
|
+
### Update mutation with specific query invalidation
|
|
67
62
|
|
|
68
63
|
```typescript
|
|
69
|
-
export function useUpdateContact() {
|
|
70
|
-
return useMutation
|
|
71
|
-
queryFn: async (
|
|
72
|
-
return await ContactService.update(
|
|
64
|
+
export function useUpdateContact(contactUuid: string) {
|
|
65
|
+
return useMutation({
|
|
66
|
+
queryFn: async ({ body }: { body: ContactUpdateForm }) => {
|
|
67
|
+
return await ContactService.update(contactUuid, body)
|
|
73
68
|
},
|
|
74
69
|
queryKeysToInvalidate: {
|
|
75
|
-
// Invalidate only the specific contact that was updated
|
|
76
70
|
contactDetail: {
|
|
77
|
-
contactUuid: (
|
|
71
|
+
contactUuid: (_params, _result) => contactUuid, // Invalidate only this contact's detail query
|
|
78
72
|
},
|
|
79
|
-
// Invalidate all
|
|
80
|
-
contactList: {},
|
|
81
|
-
},
|
|
82
|
-
})
|
|
83
|
-
}
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
Param extractors receive `(mutationParams, responseData)` and return the value for that query param. Empty object `{}` invalidates all queries with that key.
|
|
87
|
-
|
|
88
|
-
### Mutation with URL params only (no body)
|
|
89
|
-
|
|
90
|
-
```typescript
|
|
91
|
-
export function useDeleteContact() {
|
|
92
|
-
return useMutation<void, void, { contactUuid: string }>({
|
|
93
|
-
queryFn: async (options) => {
|
|
94
|
-
return await ContactService.delete(options.params.contactUuid)
|
|
95
|
-
},
|
|
96
|
-
queryKeysToInvalidate: {
|
|
97
|
-
contactList: {},
|
|
73
|
+
contactList: {}, // Invalidate all contactList queries
|
|
98
74
|
},
|
|
99
75
|
})
|
|
100
76
|
}
|
|
101
|
-
|
|
102
|
-
// execute({ params: { contactUuid: '123' } })
|
|
103
77
|
```
|
|
104
78
|
|
|
105
|
-
|
|
79
|
+
You can invalidate multiple queries. Include queries that depend on the data you're changing.
|
|
106
80
|
|
|
107
81
|
### Form integration
|
|
108
82
|
|
|
109
83
|
```vue
|
|
110
84
|
<script setup lang="ts">
|
|
111
|
-
import {
|
|
85
|
+
import { ref } from 'vue'
|
|
112
86
|
import { useCreateContact } from '@/composables'
|
|
113
87
|
|
|
114
88
|
const form = reactive({ name: '', email: '' })
|
|
@@ -131,8 +105,8 @@ async function handleSubmit() {
|
|
|
131
105
|
<button :disabled="result.isLoading()">
|
|
132
106
|
{{ result.isLoading() ? 'Creating...' : 'Create' }}
|
|
133
107
|
</button>
|
|
134
|
-
<div v-if="result.isErr()">
|
|
135
|
-
Error: {{ result.getError().errors
|
|
108
|
+
<div v-if="result.isErr() && 'errors' in result.getError()">
|
|
109
|
+
Error: {{ result.getError().errors[0].detail }}
|
|
136
110
|
</div>
|
|
137
111
|
</form>
|
|
138
112
|
</template>
|
|
@@ -140,125 +114,6 @@ async function handleSubmit() {
|
|
|
140
114
|
|
|
141
115
|
Use `result.isLoading()` to disable the button during mutation.
|
|
142
116
|
|
|
143
|
-
## Common Mistakes
|
|
144
|
-
|
|
145
|
-
### CRITICAL: Import useMutation from @tanstack/vue-query instead of your api module
|
|
146
|
-
|
|
147
|
-
```typescript
|
|
148
|
-
// ❌ Wrong: using TanStack directly
|
|
149
|
-
import { useMutation } from '@tanstack/vue-query'
|
|
150
|
-
|
|
151
|
-
const mutation = useMutation({
|
|
152
|
-
mutationFn: async (data) => ContactService.create(data),
|
|
153
|
-
onSuccess: () => {
|
|
154
|
-
queryClient.invalidateQueries({ queryKey: ['contactList'] })
|
|
155
|
-
},
|
|
156
|
-
})
|
|
157
|
-
// Loses AsyncResult, type safety, error codes
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
```typescript
|
|
161
|
-
// ✅ Correct: use the composable from your api module
|
|
162
|
-
import { useMutation } from '@/api'
|
|
163
|
-
|
|
164
|
-
const { execute, result } = useMutation({
|
|
165
|
-
queryFn: async (options: { body: ContactCreateForm }) => {
|
|
166
|
-
return await ContactService.create(options.body)
|
|
167
|
-
},
|
|
168
|
-
queryKeysToInvalidate: {
|
|
169
|
-
contactList: {}, // Typed, type-safe
|
|
170
|
-
},
|
|
171
|
-
})
|
|
172
|
-
// Full AsyncResult, type-safe queryKeysToInvalidate, error codes
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
Direct TanStack import loses type safety and AsyncResult wrapping.
|
|
176
|
-
|
|
177
|
-
Source: `src/composables/mutation/mutation.composable.ts`
|
|
178
|
-
|
|
179
|
-
### CRITICAL: Forget to list queryKeysToInvalidate; cache becomes stale
|
|
180
|
-
|
|
181
|
-
```typescript
|
|
182
|
-
// ❌ Wrong: no queryKeysToInvalidate
|
|
183
|
-
const { execute } = useMutation({
|
|
184
|
-
queryFn: async (options: { body: ContactCreateForm }) => {
|
|
185
|
-
return await ContactService.create(options.body)
|
|
186
|
-
},
|
|
187
|
-
// Forgot queryKeysToInvalidate!
|
|
188
|
-
})
|
|
189
|
-
// Mutation succeeds but list query still shows old data
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
```typescript
|
|
193
|
-
// ✅ Correct: invalidate affected queries
|
|
194
|
-
const { execute } = useMutation({
|
|
195
|
-
queryFn: async (options: { body: ContactCreateForm }) => {
|
|
196
|
-
return await ContactService.create(options.body)
|
|
197
|
-
},
|
|
198
|
-
queryKeysToInvalidate: {
|
|
199
|
-
contactList: {}, // Invalidate all contactList queries
|
|
200
|
-
},
|
|
201
|
-
})
|
|
202
|
-
// After success, contactList queries refetch
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
If you don't list which queries to invalidate, the cache stays stale and the UI shows outdated data.
|
|
206
|
-
|
|
207
|
-
Source: `src/composables/mutation/mutation.composable.ts` — `onSuccess` invalidation logic
|
|
208
|
-
|
|
209
|
-
### HIGH: Not await execute(); code runs before mutation completes
|
|
210
|
-
|
|
211
|
-
```typescript
|
|
212
|
-
// ❌ Wrong: fire and forget
|
|
213
|
-
async function handleSubmit() {
|
|
214
|
-
execute({ body: formData })
|
|
215
|
-
router.push('/contacts') // Redirects before mutation finishes!
|
|
216
|
-
}
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
```typescript
|
|
220
|
-
// ✅ Correct: await the result
|
|
221
|
-
async function handleSubmit() {
|
|
222
|
-
const result = await execute({ body: formData })
|
|
223
|
-
if (result.isOk()) {
|
|
224
|
-
router.push('/contacts')
|
|
225
|
-
}
|
|
226
|
-
// If isErr, form stays visible for retry
|
|
227
|
-
}
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
Not awaiting `execute()` means the mutation is still in flight when you navigate away or access the result.
|
|
231
|
-
|
|
232
|
-
Source: `src/composables/mutation/mutation.composable.ts` — `execute` returns `Promise<ApiResult>`
|
|
233
|
-
|
|
234
|
-
### HIGH: Use body instead of params for URL parameters
|
|
235
|
-
|
|
236
|
-
```typescript
|
|
237
|
-
// ❌ Wrong: URL params passed as body
|
|
238
|
-
const { execute } = useMutation<SearchForm, Results, void>({
|
|
239
|
-
queryFn: async (options) => {
|
|
240
|
-
return await SearchService.search(options.body) // URL params shouldn't be in body
|
|
241
|
-
},
|
|
242
|
-
})
|
|
243
|
-
```
|
|
244
|
-
|
|
245
|
-
```typescript
|
|
246
|
-
// ✅ Correct: separate body (payload) from params (URL query string)
|
|
247
|
-
const { execute } = useMutation<SearchForm, Results, { category: string }>({
|
|
248
|
-
queryFn: async (options) => {
|
|
249
|
-
const { body, params } = options
|
|
250
|
-
return await SearchService.search(body, params.category)
|
|
251
|
-
},
|
|
252
|
-
})
|
|
253
|
-
|
|
254
|
-
// Call with both:
|
|
255
|
-
execute({ body: searchForm, params: { category: 'contacts' } })
|
|
256
|
-
```
|
|
257
|
-
|
|
258
|
-
`body` is for the request payload (POST/PUT body); `params` is for URL query string parameters. The `RequestParams` type enforces this shape automatically based on your generics.
|
|
259
|
-
|
|
260
|
-
Source: `src/composables/mutation/mutation.composable.ts` — `RequestParams` type
|
|
261
|
-
|
|
262
117
|
## See Also
|
|
263
118
|
|
|
264
119
|
- [Cache Management](../cache-management/SKILL.md) — Understanding which queries to invalidate
|
|
@@ -4,9 +4,6 @@ description: >
|
|
|
4
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: "1.2.0"
|
|
8
|
-
sources:
|
|
9
|
-
- "wisemen-digital/wisemen-core:packages/web/api-utils/src/composables/query/query.composable.ts"
|
|
10
7
|
---
|
|
11
8
|
|
|
12
9
|
# @wisemen/vue-core-api-utils — Writing Queries
|
|
@@ -71,20 +68,6 @@ const { result } = useQuery('contactDetail', {
|
|
|
71
68
|
|
|
72
69
|
`staleTime` determines how long cached data is considered fresh. After this time, the next query interaction triggers a background refetch.
|
|
73
70
|
|
|
74
|
-
### Conditionally enable a query
|
|
75
|
-
|
|
76
|
-
```typescript
|
|
77
|
-
const isEnabled = computed(() => contactUuid.value !== null)
|
|
78
|
-
|
|
79
|
-
const { result } = useQuery('contactDetail', {
|
|
80
|
-
params: { contactUuid: computed(() => contactUuid.value!) },
|
|
81
|
-
queryFn: () => ContactService.getByUuid(contactUuid.value!),
|
|
82
|
-
isEnabled,
|
|
83
|
-
})
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
Use `isEnabled` to prevent the query from running until required data is available.
|
|
87
|
-
|
|
88
71
|
### Manually refetch on demand
|
|
89
72
|
|
|
90
73
|
```typescript
|
|
@@ -102,115 +85,6 @@ if (result.value.isOk()) {
|
|
|
102
85
|
}
|
|
103
86
|
```
|
|
104
87
|
|
|
105
|
-
## Common Mistakes
|
|
106
|
-
|
|
107
|
-
### CRITICAL: Import useQuery from @tanstack/vue-query instead of your api module
|
|
108
|
-
|
|
109
|
-
```typescript
|
|
110
|
-
// ❌ Wrong: using TanStack directly
|
|
111
|
-
import { useQuery } from '@tanstack/vue-query'
|
|
112
|
-
|
|
113
|
-
const { data, error, isLoading } = useQuery({
|
|
114
|
-
queryKey: ['contactDetail', '123'],
|
|
115
|
-
queryFn: () => ContactService.getByUuid('123'),
|
|
116
|
-
})
|
|
117
|
-
// Loses AsyncResult wrapping, type safety, error code typing
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
```typescript
|
|
121
|
-
// ✅ Correct: use the composable from your api module (or directly from the library)
|
|
122
|
-
import { useQuery } from '@/api'
|
|
123
|
-
import { computed } from 'vue'
|
|
124
|
-
|
|
125
|
-
const { result, isLoading } = useQuery('contactDetail', {
|
|
126
|
-
params: { contactUuid: computed(() => '123') },
|
|
127
|
-
queryFn: () => ContactService.getByUuid('123'),
|
|
128
|
-
staleTime: 1000 * 60 * 5,
|
|
129
|
-
})
|
|
130
|
-
// Full type safety, AsyncResult wrapping, automatic error codes
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
Importing directly from @tanstack/vue-query bypasses the typed composable, losing AsyncResult wrapping, type-safe query keys, and error code typing.
|
|
134
|
-
|
|
135
|
-
Source: `src/composables/query/query.composable.ts`
|
|
136
|
-
|
|
137
|
-
### HIGH: Use plain ref for params instead of computed
|
|
138
|
-
|
|
139
|
-
```typescript
|
|
140
|
-
// ❌ Wrong: plain ref doesn't trigger refetch
|
|
141
|
-
const userId = ref('123')
|
|
142
|
-
const { result } = useQuery('userDetail', {
|
|
143
|
-
params: { userId }, // plain ref, not computed
|
|
144
|
-
queryFn: () => UserService.getById(userId.value),
|
|
145
|
-
})
|
|
146
|
-
// Later: userId.value = '456'
|
|
147
|
-
// Query does NOT refetch — cache stays stale!
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
```typescript
|
|
151
|
-
// ✅ Correct: use computed so query watches changes
|
|
152
|
-
const userId = computed(() => props.userId)
|
|
153
|
-
const { result } = useQuery('userDetail', {
|
|
154
|
-
params: { userId },
|
|
155
|
-
queryFn: () => UserService.getById(userId.value),
|
|
156
|
-
})
|
|
157
|
-
// userId changes → computed updates → query watches → refetch happens
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
When params are plain refs, the query doesn't watch them and the cache isn't invalidated when the param changes.
|
|
161
|
-
|
|
162
|
-
Source: `src/composables/query/query.composable.ts` — `NestedMaybeRefOrGetter` type
|
|
163
|
-
|
|
164
|
-
### HIGH: Not set staleTime; background refetch on every interaction
|
|
165
|
-
|
|
166
|
-
```typescript
|
|
167
|
-
// ❌ Wrong: no staleTime; background refetch constantly
|
|
168
|
-
const { result } = useQuery('userDetail', {
|
|
169
|
-
params: { userId: computed(() => '123') },
|
|
170
|
-
queryFn: () => UserService.getById('123'),
|
|
171
|
-
// staleTime defaults to 0 — cache is immediately stale!
|
|
172
|
-
})
|
|
173
|
-
// Every component interaction triggers a refetch
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
```typescript
|
|
177
|
-
// ✅ Correct: set staleTime to a reasonable value
|
|
178
|
-
const { result } = useQuery('userDetail', {
|
|
179
|
-
params: { userId: computed(() => '123') },
|
|
180
|
-
queryFn: () => UserService.getById('123'),
|
|
181
|
-
staleTime: 1000 * 60 * 5, // 5 minutes
|
|
182
|
-
})
|
|
183
|
-
// Cache remains fresh for 5 minutes — background refetch only after expiry
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
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.
|
|
187
|
-
|
|
188
|
-
### MEDIUM: Confuse isFetching with isLoading
|
|
189
|
-
|
|
190
|
-
```typescript
|
|
191
|
-
// ❌ Wrong: checking isLoading for conditional render
|
|
192
|
-
const { result, isLoading } = useQuery(...)
|
|
193
|
-
if (isLoading.value) {
|
|
194
|
-
return // Exits only on initial load!
|
|
195
|
-
}
|
|
196
|
-
// Code here runs while background refetch happens
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
```typescript
|
|
200
|
-
// ✅ Correct: use result.isLoading() for state checks
|
|
201
|
-
const { result } = useQuery(...)
|
|
202
|
-
if (result.value.isLoading()) {
|
|
203
|
-
return // True only on initial load
|
|
204
|
-
}
|
|
205
|
-
// Use isFetching separately for background fetch indicator
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
`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.
|
|
209
|
-
|
|
210
|
-
Note: `isLoading`, `isError`, and `isSuccess` on the return type are deprecated — prefer `result.value.isLoading()`, `result.value.isErr()`, and `result.value.isOk()`.
|
|
211
|
-
|
|
212
|
-
Source: `src/composables/query/query.composable.ts` — `UseQueryReturnType`
|
|
213
|
-
|
|
214
88
|
## See Also
|
|
215
89
|
|
|
216
90
|
- [Cache Management](../cache-management/SKILL.md) — Understanding caching strategy informs staleTime choices
|