@wisemen/vue-core-api-utils 2.0.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.
@@ -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 key) for real-time data and large datasets.
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 (offset/limit-based)
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: (pagination) => ContactService.getAll({
40
- offset: pagination.offset,
41
- limit: pagination.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: number, limit: number }`. Offset starts at 0 and advances by `limit` for each next page. Return results with `{ data: Contact[], meta: { offset, limit, total } }`.
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: (pagination) => ContactService.getAllKeyset({
65
- limit: pagination.limit,
66
- key: pagination.key,
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?: any, limit: number }`. The `key` is the cursor value from the previous page's `meta.next`, or `undefined` for the first page. Return results with `{ data: Contact[], meta: { next: unknown } }` set `meta.next` to `null`/`undefined` when there are no more pages.
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 accepts `offset` and `limit` parameters, use `useOffsetInfiniteQuery`.
305
- If your API accepts a cursor `key` parameter, use `useKeysetInfiniteQuery`.
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 with optional param extractors, AsyncResult error handling, execute function, request shape with body/params separation.
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 (options: { body: ContactCreateForm }) => {
25
- return await ContactService.create(options.body)
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 should list which queries to invalidate via `queryKeysToInvalidate`.
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 using param extractors
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<ContactUpdateForm, Contact, { contactUuid: string }>({
71
- queryFn: async (options) => {
72
- return await ContactService.update(options.params.contactUuid, options.body)
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: (params) => params.contactUuid,
71
+ contactUuid: (_params, _result) => contactUuid, // Invalidate only this contact's detail query
78
72
  },
79
- // Invalidate all contact lists
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
- When `TReqData` is `void`, the `execute` call takes `{ params: TParams }` instead of `{ body, params }`.
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 { reactive } from 'vue'
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?.[0]?.detail }}
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