@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,11 +4,6 @@ description: >
|
|
|
4
4
|
Three-state AsyncResult type (Loading, Ok, Err), isLoading/isOk/isErr type predicates, getValue/getError accessors, match() pattern matching, map/mapErr transformations, safe value extraction without undefined.
|
|
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/concepts/result-types.md"
|
|
10
|
-
- "wisemen-digital/wisemen-core:docs/packages/api-utils/pages/usage/overview.md"
|
|
11
|
-
- "wisemen-digital/wisemen-core:packages/web/api-utils/src/async-result/asyncResult.ts"
|
|
12
7
|
---
|
|
13
8
|
|
|
14
9
|
# @wisemen/vue-core-api-utils — Handling AsyncResult Types
|
|
@@ -101,80 +96,6 @@ const name = result.value
|
|
|
101
96
|
// Type: string
|
|
102
97
|
```
|
|
103
98
|
|
|
104
|
-
## Common Mistakes
|
|
105
|
-
|
|
106
|
-
### CRITICAL: Forget to check state before calling getValue/getError
|
|
107
|
-
|
|
108
|
-
```typescript
|
|
109
|
-
// ❌ Wrong: getValue without isOk check
|
|
110
|
-
const { result } = useQuery('contactDetail', { /* ... */ })
|
|
111
|
-
const contact = result.value.getValue()
|
|
112
|
-
console.log(contact.name) // contact could be null!
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
```typescript
|
|
116
|
-
// ✅ Correct: check isOk first
|
|
117
|
-
const { result } = useQuery('contactDetail', { /* ... */ })
|
|
118
|
-
if (result.value.isOk()) {
|
|
119
|
-
const contact = result.value.getValue()
|
|
120
|
-
console.log(contact.name) // Safe!
|
|
121
|
-
}
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
Calling `getValue()` without `isOk()` returns null if the result is loading or an error. You get no compile error, and the UI renders nothing or crashes at runtime.
|
|
125
|
-
|
|
126
|
-
Source: `docs/packages/api-utils/pages/concepts/result-types.md`
|
|
127
|
-
|
|
128
|
-
### HIGH: Not handle all three states in match()
|
|
129
|
-
|
|
130
|
-
```typescript
|
|
131
|
-
// ❌ Wrong: missing loading handler
|
|
132
|
-
result.value.match({
|
|
133
|
-
ok: (data) => <div>{data.name}</div>,
|
|
134
|
-
err: (error) => <div>Error: {error.detail}</div>,
|
|
135
|
-
// Forgot loading!
|
|
136
|
-
})
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
```typescript
|
|
140
|
-
// ✅ Correct: handle all three states
|
|
141
|
-
result.value.match({
|
|
142
|
-
loading: () => <div>Loading...</div>,
|
|
143
|
-
ok: (data) => <div>{data.name}</div>,
|
|
144
|
-
err: (error) => <div>Error: {error.detail}</div>,
|
|
145
|
-
})
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
If you omit a handler, TypeScript errors and the UI renders nothing during the omitted state. The match is exhaustive by design.
|
|
149
|
-
|
|
150
|
-
Source: `docs/packages/api-utils/pages/concepts/result-types.md` Pattern Matching Section
|
|
151
|
-
|
|
152
|
-
### HIGH: Use state flags (isLoading, isError, isSuccess) instead of AsyncResult state
|
|
153
|
-
|
|
154
|
-
```typescript
|
|
155
|
-
// ❌ Wrong: mixing old flags with AsyncResult
|
|
156
|
-
const { result, isLoading } = useQuery(...)
|
|
157
|
-
if (isLoading.value) {
|
|
158
|
-
// Show spinner
|
|
159
|
-
} else {
|
|
160
|
-
const data = result.value.getValue() // Could be null!
|
|
161
|
-
}
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
```typescript
|
|
165
|
-
// ✅ Correct: use AsyncResult state exclusively
|
|
166
|
-
const { result } = useQuery(...)
|
|
167
|
-
if (result.value.isLoading()) {
|
|
168
|
-
// Show spinner
|
|
169
|
-
} else if (result.value.isOk()) {
|
|
170
|
-
const data = result.value.getValue() // Safe!
|
|
171
|
-
}
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
Composables export both AsyncResult (exhaustive) and backward-compatible flags (`isLoading`, `isError`, `isSuccess`). Mixing them causes logic bugs where flags say the query is done but the result is still loading.
|
|
175
|
-
|
|
176
|
-
Source: Maintainer interview — library provides both patterns for backward compatibility, but agents should prefer AsyncResult
|
|
177
|
-
|
|
178
99
|
## Next Steps
|
|
179
100
|
|
|
180
101
|
- [Writing Queries](../writing-queries/SKILL.md) — Fetch single resources with caching
|
|
@@ -4,16 +4,11 @@ description: >
|
|
|
4
4
|
Type-safe QueryClient with get/set/update/invalidate methods, predicate-based updates, cascade invalidation strategy, shared cache across components, lazy refetch patterns.
|
|
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-client.md"
|
|
10
|
-
- "wisemen-digital/wisemen-core:packages/web/api-utils/src/utils/query-client/queryClient.ts"
|
|
11
|
-
- "wisemen-digital/wisemen-core:packages/web/api-utils/src/factory/createApiQueryClientUtils.ts"
|
|
12
7
|
---
|
|
13
8
|
|
|
14
9
|
# @wisemen/vue-core-api-utils — Cache Management
|
|
15
10
|
|
|
16
|
-
Manually read, write, update, and invalidate the query cache using the type-safe `
|
|
11
|
+
Manually read, write, update, and invalidate the query cache using the type-safe `QueryClient` wrapper. This is useful for optimistic updates and strategically invalidating affected queries.
|
|
17
12
|
|
|
18
13
|
## Setup
|
|
19
14
|
|
|
@@ -31,16 +26,18 @@ queryClient.set(
|
|
|
31
26
|
updatedContact
|
|
32
27
|
)
|
|
33
28
|
|
|
34
|
-
// Update cached data with a predicate
|
|
35
|
-
queryClient.update('contactList', {
|
|
29
|
+
// Update cached data with a predicate (returns { rollback } for reverting)
|
|
30
|
+
const { rollback } = queryClient.update('contactList', {
|
|
36
31
|
by: (contact) => contact.id === '123',
|
|
37
32
|
value: (contact) => ({ ...contact, name: 'Updated' }),
|
|
38
33
|
})
|
|
39
34
|
|
|
40
|
-
// Invalidate queries (
|
|
41
|
-
queryClient.invalidate('contactList')
|
|
35
|
+
// Invalidate queries (async — triggers refetch)
|
|
36
|
+
await queryClient.invalidate('contactList')
|
|
42
37
|
```
|
|
43
38
|
|
|
39
|
+
`useQueryClient()` is a helper you create in your `@/api` module (see [getting-started](../getting-started/SKILL.md)) that wraps `new QueryClient(getTanstackQueryClient())`.
|
|
40
|
+
|
|
44
41
|
## Core Patterns
|
|
45
42
|
|
|
46
43
|
### Get cached data
|
|
@@ -86,8 +83,8 @@ queryClient.set('contactList', [
|
|
|
86
83
|
```typescript
|
|
87
84
|
const queryClient = useQueryClient()
|
|
88
85
|
|
|
89
|
-
// Update a single item in a list
|
|
90
|
-
queryClient.update('contactList', {
|
|
86
|
+
// Update a single item in a list — returns { rollback } for reverting
|
|
87
|
+
const { rollback } = queryClient.update('contactList', {
|
|
91
88
|
by: (contact) => contact.id === '123', // Predicate
|
|
92
89
|
value: (contact) => ({ // Transform
|
|
93
90
|
...contact,
|
|
@@ -96,13 +93,13 @@ queryClient.update('contactList', {
|
|
|
96
93
|
})
|
|
97
94
|
|
|
98
95
|
// For single entities, the predicate always matches
|
|
99
|
-
queryClient.update('contactDetail', {
|
|
100
|
-
by: (contact) => true,
|
|
96
|
+
const { rollback: rollbackDetail } = queryClient.update('contactDetail', {
|
|
97
|
+
by: (contact) => true,
|
|
101
98
|
value: (contact) => ({ ...contact, name: 'Updated' }),
|
|
102
99
|
})
|
|
103
100
|
```
|
|
104
101
|
|
|
105
|
-
QueryClient knows whether the entity is an array or single item, so predicates work transparently on lists.
|
|
102
|
+
`update()` returns `{ rollback }` — a function that reverts the cache to its previous state. Use this for optimistic updates. QueryClient knows whether the entity is an array or single item, so predicates work transparently on lists.
|
|
106
103
|
|
|
107
104
|
### Invalidate and refetch
|
|
108
105
|
|
|
@@ -110,78 +107,16 @@ QueryClient knows whether the entity is an array or single item, so predicates w
|
|
|
110
107
|
const queryClient = useQueryClient()
|
|
111
108
|
|
|
112
109
|
// Invalidate all queries with this key
|
|
113
|
-
queryClient.invalidate('contactList')
|
|
110
|
+
await queryClient.invalidate('contactList')
|
|
114
111
|
|
|
115
112
|
// Invalidate specific query
|
|
116
|
-
queryClient.invalidate(['contactDetail', { contactUuid: '123' }])
|
|
113
|
+
await queryClient.invalidate(['contactDetail', { contactUuid: '123' }])
|
|
117
114
|
|
|
118
115
|
// After invalidation, the next query interaction triggers a refetch
|
|
119
116
|
```
|
|
120
117
|
|
|
121
118
|
Invalidation marks cached data as stale. The next interaction (component mount, user action) triggers a refetch.
|
|
122
119
|
|
|
123
|
-
## Common Mistakes
|
|
124
|
-
|
|
125
|
-
### HIGH: Call update/set without checking data structure (array vs entity)
|
|
126
|
-
|
|
127
|
-
```typescript
|
|
128
|
-
// ❌ Wrong: treating array like entity
|
|
129
|
-
const queryClient = useQueryClient()
|
|
130
|
-
queryClient.update('contactList', {
|
|
131
|
-
by: (contact) => contact.id === '123',
|
|
132
|
-
value: (contact) => ({ ...contact, name: 'Updated' }),
|
|
133
|
-
})
|
|
134
|
-
// If contactList is Contact[], predicate matches each item individually
|
|
135
|
-
// If you expect single match, this breaks silently
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
```typescript
|
|
139
|
-
// ✅ Correct: QueryClient infers type from query key
|
|
140
|
-
const queryClient = useQueryClient()
|
|
141
|
-
// If contactList: { entity: Contact[], ... }
|
|
142
|
-
// QueryClient knows to iterate the array and apply predicate to each item
|
|
143
|
-
queryClient.update('contactList', {
|
|
144
|
-
by: (contact) => contact.id === '123',
|
|
145
|
-
value: (contact) => ({ ...contact, name: 'Updated' }),
|
|
146
|
-
})
|
|
147
|
-
// For single entities: { entity: Contact, ... }
|
|
148
|
-
// QueryClient provides the entity directly to predicate
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
QueryClient infers data structure from your query key definition, so the same `update()` call works correctly for arrays and single entities. The type system ensures you're using the right predicate signature.
|
|
152
|
-
|
|
153
|
-
Source: `docs/packages/api-utils/pages/usage/query-client.md` Usage
|
|
154
|
-
|
|
155
|
-
### MEDIUM: Call set() without async loading state; UI flashes stale data
|
|
156
|
-
|
|
157
|
-
```typescript
|
|
158
|
-
// ❌ Wrong: immediate set without loading indicator
|
|
159
|
-
const queryClient = useQueryClient()
|
|
160
|
-
queryClient.set(['contactDetail', { contactUuid }], updatedData)
|
|
161
|
-
// Cache updated but no indicator that request is pending
|
|
162
|
-
// UI looks responsive but actually has unconfirmed data
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
```typescript
|
|
166
|
-
// ✅ Correct: pair optimistic update with mutation result handling
|
|
167
|
-
const queryClient = useQueryClient()
|
|
168
|
-
const original = queryClient.get(['contactDetail', { contactUuid }])
|
|
169
|
-
|
|
170
|
-
// Update cache optimistically
|
|
171
|
-
queryClient.set(['contactDetail', { contactUuid }], newData)
|
|
172
|
-
|
|
173
|
-
// Execute mutation
|
|
174
|
-
const result = await execute(formData)
|
|
175
|
-
if (result.isErr()) {
|
|
176
|
-
// Rollback on error
|
|
177
|
-
queryClient.set(['contactDetail', { contactUuid }], original)
|
|
178
|
-
}
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
If you update the cache without a mutation in flight, the UI looks responsive but the data is unconfirmed. Always pair cache updates with mutation execution and rollback on error.
|
|
182
|
-
|
|
183
|
-
Source: `docs/packages/api-utils/pages/usage/query-client.md` Real-World Example
|
|
184
|
-
|
|
185
120
|
## Cache Strategy
|
|
186
121
|
|
|
187
122
|
> Explicitly invalidate only the queries affected by the mutation. Let lazy refetch handle the rest when users navigate to pages needing other data.
|
|
@@ -4,12 +4,6 @@ description: >
|
|
|
4
4
|
neverthrow Result architectural basis; three-state AsyncResult relationship to Result; @tanstack/vue-query lifecycle (staleTime, gcTime, refetch); composition of TanStack Query + neverthrow + Vue 3 reactivity.
|
|
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/concepts/result-types.md"
|
|
10
|
-
- "wisemen-digital/wisemen-core:packages/web/api-utils/src/async-result/asyncResult.ts"
|
|
11
|
-
- "wisemen-digital/wisemen-core:packages/web/api-utils/src/types/apiError.type.ts"
|
|
12
|
-
- "wisemen-digital/wisemen-core:packages/web/api-utils/src/config/config.ts"
|
|
13
7
|
---
|
|
14
8
|
|
|
15
9
|
# @wisemen/vue-core-api-utils — Foundations
|
|
@@ -197,8 +191,8 @@ async function handleRefresh() {
|
|
|
197
191
|
|
|
198
192
|
// Automatic refetch on mutation (via queryKeysToInvalidate)
|
|
199
193
|
const { execute } = useMutation({
|
|
200
|
-
queryFn: (
|
|
201
|
-
queryKeysToInvalidate: { contactDetail:
|
|
194
|
+
queryFn: ({ body }: { body: ContactUpdateForm }) => ContactService.update(body),
|
|
195
|
+
queryKeysToInvalidate: { contactDetail: {} },
|
|
202
196
|
})
|
|
203
197
|
// After execute succeeds, contactDetail is invalidated
|
|
204
198
|
// Next useQuery('contactDetail') refetches fresh data
|
|
@@ -246,8 +240,9 @@ Errors are typed and structured using `neverthrow`:
|
|
|
246
240
|
interface ApiExpectedError {
|
|
247
241
|
errors: Array<{
|
|
248
242
|
code: string
|
|
249
|
-
|
|
250
|
-
|
|
243
|
+
detail: string
|
|
244
|
+
status: string
|
|
245
|
+
source?: { pointer: string }
|
|
251
246
|
}>
|
|
252
247
|
}
|
|
253
248
|
|
|
@@ -259,10 +254,11 @@ const result = new AsyncResultErr(apiError)
|
|
|
259
254
|
result.match({
|
|
260
255
|
ok: (data) => {}, // not executed
|
|
261
256
|
err: (error) => {
|
|
262
|
-
// error is ApiError
|
|
263
|
-
if (
|
|
257
|
+
// error is ApiError — narrow with 'errors' in error
|
|
258
|
+
if ('errors' in error) {
|
|
264
259
|
// Handle known API errors
|
|
265
260
|
const codes = error.errors.map(e => e.code)
|
|
261
|
+
const detail = error.errors[0].detail
|
|
266
262
|
} else {
|
|
267
263
|
// Handle network/parsing errors
|
|
268
264
|
console.error(error.message)
|
|
@@ -273,154 +269,6 @@ result.match({
|
|
|
273
269
|
|
|
274
270
|
Error types are defined at library initialization via the generic `TErrorCode`. This ensures type-safe error handling across queries and mutations.
|
|
275
271
|
|
|
276
|
-
## Common Mistakes
|
|
277
|
-
|
|
278
|
-
### CRITICAL: Confuse Result (neverthrow) with AsyncResult; treat ok/err as boolean
|
|
279
|
-
|
|
280
|
-
```typescript
|
|
281
|
-
// ❌ Wrong: neverthrow Result is not AsyncResult
|
|
282
|
-
const result = new Result(contact, null) // This is not how neverthrow works
|
|
283
|
-
if (result.ok) { // `.ok` doesn't exist
|
|
284
|
-
console.log(result.value)
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// Or even worse: treating AsyncResult like a boolean
|
|
288
|
-
const { result } = useQuery(...)
|
|
289
|
-
if (result.value) {
|
|
290
|
-
// This is always true; result is always defined (Loading | Ok | Err)
|
|
291
|
-
}
|
|
292
|
-
```
|
|
293
|
-
|
|
294
|
-
```typescript
|
|
295
|
-
// ✅ Correct: AsyncResult requires exhaustive pattern matching
|
|
296
|
-
const { result } = useQuery('contactDetail', {
|
|
297
|
-
queryFn: () => ContactService.getDetail(uuid),
|
|
298
|
-
})
|
|
299
|
-
|
|
300
|
-
result.value.match({
|
|
301
|
-
loading: () => showSpinner(),
|
|
302
|
-
ok: (contact) => showContact(contact),
|
|
303
|
-
err: (error) => showError(error),
|
|
304
|
-
})
|
|
305
|
-
|
|
306
|
-
// Or use type guards
|
|
307
|
-
if (result.value.isOk()) {
|
|
308
|
-
console.log(result.value.getValue())
|
|
309
|
-
} else if (result.value.isErr()) {
|
|
310
|
-
console.log(result.value.getError())
|
|
311
|
-
} else {
|
|
312
|
-
showSpinner()
|
|
313
|
-
}
|
|
314
|
-
```
|
|
315
|
-
|
|
316
|
-
AsyncResult requires explicit handling of all three states. The type system won't let you skip a state.
|
|
317
|
-
|
|
318
|
-
Source: `docs/packages/api-utils/pages/concepts/result-types.md`
|
|
319
|
-
|
|
320
|
-
### MEDIUM: Misunderstand staleTime; think data refreshes automatically after staleTime
|
|
321
|
-
|
|
322
|
-
```typescript
|
|
323
|
-
// ❌ Wrong: assuming staleTime auto-refetches
|
|
324
|
-
const { result } = useQuery('contactDetail', {
|
|
325
|
-
queryFn: () => ContactService.getDetail(uuid),
|
|
326
|
-
staleTime: 5 * 60 * 1000, // Not an auto-refresh interval
|
|
327
|
-
})
|
|
328
|
-
|
|
329
|
-
// At T=5m, data doesn't automatically refetch.
|
|
330
|
-
// It's just marked stale. Refetch happens on next interaction
|
|
331
|
-
// (component mount, user action, mutation invalidation)
|
|
332
|
-
```
|
|
333
|
-
|
|
334
|
-
```typescript
|
|
335
|
-
// ✅ Correct: staleTime is a grace period, not an interval
|
|
336
|
-
const { result, refetch } = useQuery('contactDetail', {
|
|
337
|
-
queryFn: () => ContactService.getDetail(uuid),
|
|
338
|
-
staleTime: 5 * 60 * 1000,
|
|
339
|
-
})
|
|
340
|
-
|
|
341
|
-
// Data is fresh for 5 minutes (instant returns)
|
|
342
|
-
// After 5 minutes, next access triggers refetch
|
|
343
|
-
// For auto-refresh, use refetch() in a watchEffect or timer
|
|
344
|
-
|
|
345
|
-
watchEffect(async () => {
|
|
346
|
-
// Refetch every 10 seconds
|
|
347
|
-
const interval = setInterval(() => refetch(), 10 * 1000)
|
|
348
|
-
onCleanup(() => clearInterval(interval))
|
|
349
|
-
})
|
|
350
|
-
```
|
|
351
|
-
|
|
352
|
-
Stale time is not an auto-refresh interval. It's the duration the cache is considered fresh without refetching. Refetch happens on next access or when explicitly triggered.
|
|
353
|
-
|
|
354
|
-
### HIGH: Misunderstand gcTime and cache eviction; assume cache persists forever
|
|
355
|
-
|
|
356
|
-
```typescript
|
|
357
|
-
// ❌ Wrong: assuming cache is permanent
|
|
358
|
-
const { result } = useQuery('contactDetail', {
|
|
359
|
-
queryFn: () => ContactService.getDetail(uuid),
|
|
360
|
-
gcTime: 5 * 60 * 1000, // Default is 5 minutes
|
|
361
|
-
})
|
|
362
|
-
|
|
363
|
-
// If component unmounts and user is gone for > 5 minutes,
|
|
364
|
-
// Next access refetches fresh (cache is evicted)
|
|
365
|
-
// This is correct behavior, but if you expected cached data...
|
|
366
|
-
```
|
|
367
|
-
|
|
368
|
-
```typescript
|
|
369
|
-
// ✅ Correct: increase gcTime if you want longer-lived cache
|
|
370
|
-
const { result } = useQuery('contactDetail', {
|
|
371
|
-
queryFn: () => ContactService.getDetail(uuid),
|
|
372
|
-
staleTime: 5 * 60 * 1000, // Fresh for 5 minutes
|
|
373
|
-
gcTime: 60 * 60 * 1000, // Keep in memory for 1 hour
|
|
374
|
-
})
|
|
375
|
-
|
|
376
|
-
// After 5 minutes (stale) but before 1 hour (gc),
|
|
377
|
-
// Returning cached data (but trigger refetch automatically)
|
|
378
|
-
// After 1 hour, cache is deleted; next access refetches fresh
|
|
379
|
-
```
|
|
380
|
-
|
|
381
|
-
gcTime controls cache eviction. Longer gcTime = cache lives longer. Longer staleTime = more queries use cache without refetching. Both are configurable defaults in the plugin config.
|
|
382
|
-
|
|
383
|
-
Source: `packages/web/api-utils/src/config/config.ts`
|
|
384
|
-
|
|
385
|
-
### MEDIUM: Forget that QueryClient is shared; one query invalidation affects all components
|
|
386
|
-
|
|
387
|
-
```typescript
|
|
388
|
-
// ❌ Wrong: not realizing cache is global
|
|
389
|
-
const { execute } = useMutation({
|
|
390
|
-
queryFn: () => ContactService.update(data),
|
|
391
|
-
queryKeysToInvalidate: { contactDetail: () => true },
|
|
392
|
-
})
|
|
393
|
-
|
|
394
|
-
// Component A: detail view
|
|
395
|
-
// Component B: list view (also uses contactDetail)
|
|
396
|
-
// Even though A only updated one contact,
|
|
397
|
-
// B's cache is invalidated too (because same query key)
|
|
398
|
-
```
|
|
399
|
-
|
|
400
|
-
```typescript
|
|
401
|
-
// ✅ Correct: QueryClient is intentionally shared
|
|
402
|
-
export function useContactMutation() {
|
|
403
|
-
const { execute } = useMutation({
|
|
404
|
-
queryFn: () => ContactService.update(data),
|
|
405
|
-
queryKeysToInvalidate: {
|
|
406
|
-
// Invalidates all components using this key
|
|
407
|
-
contactDetail: () => true,
|
|
408
|
-
// Also invalidate lists that show this contact
|
|
409
|
-
contactList: () => true,
|
|
410
|
-
},
|
|
411
|
-
})
|
|
412
|
-
|
|
413
|
-
return { execute }
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
// When A updates a contact, both A and B refetch.
|
|
417
|
-
// This is the intended design: shared cache across app.
|
|
418
|
-
```
|
|
419
|
-
|
|
420
|
-
QueryClient is application-wide (singleton). Invalidating a query invalidates for all components using that key. This is a feature: synchronized cache across the app.
|
|
421
|
-
|
|
422
|
-
Source: `packages/web/api-utils/src/utils/query-client/queryClient.ts`
|
|
423
|
-
|
|
424
272
|
## Integration pattern
|
|
425
273
|
|
|
426
274
|
The full integration:
|