@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.
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "access": "public"
5
5
  },
6
6
  "type": "module",
7
- "version": "2.0.0",
7
+ "version": "2.0.1",
8
8
  "license": "SEE LICENSE IN LICENSE.md",
9
9
  "repository": {
10
10
  "type": "git",
@@ -35,6 +35,7 @@
35
35
  "peerDependencies": {
36
36
  "@tanstack/vue-query": ">=5.90.5",
37
37
  "neverthrow": ">=8.2.0",
38
+ "zod": ">=4.3.5",
38
39
  "vue": ">=3.5.22"
39
40
  },
40
41
  "devDependencies": {
@@ -4,9 +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: "1.2.0"
8
- sources:
9
- - "wisemen-digital/wisemen-core:packages/web/api-utils/src/async-result/asyncResult.ts"
10
7
  ---
11
8
 
12
9
  # @wisemen/vue-core-api-utils — Handling AsyncResult Types
@@ -99,80 +96,6 @@ const name = result.value
99
96
  // Type: string
100
97
  ```
101
98
 
102
- ## Common Mistakes
103
-
104
- ### CRITICAL: Forget to check state before calling getValue/getError
105
-
106
- ```typescript
107
- // ❌ Wrong: getValue without isOk check
108
- const { result } = useQuery('contactDetail', { /* ... */ })
109
- const contact = result.value.getValue()
110
- console.log(contact.name) // contact could be null!
111
- ```
112
-
113
- ```typescript
114
- // ✅ Correct: check isOk first
115
- const { result } = useQuery('contactDetail', { /* ... */ })
116
- if (result.value.isOk()) {
117
- const contact = result.value.getValue()
118
- console.log(contact.name) // Safe!
119
- }
120
- ```
121
-
122
- 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.
123
-
124
- Source: `docs/packages/api-utils/pages/concepts/result-types.md`
125
-
126
- ### HIGH: Not handle all three states in match()
127
-
128
- ```typescript
129
- // ❌ Wrong: missing loading handler
130
- result.value.match({
131
- ok: (data) => <div>{data.name}</div>,
132
- err: (error) => <div>Error: {error.detail}</div>,
133
- // Forgot loading!
134
- })
135
- ```
136
-
137
- ```typescript
138
- // ✅ Correct: handle all three states
139
- result.value.match({
140
- loading: () => <div>Loading...</div>,
141
- ok: (data) => <div>{data.name}</div>,
142
- err: (error) => <div>Error: {error.detail}</div>,
143
- })
144
- ```
145
-
146
- If you omit a handler, TypeScript errors and the UI renders nothing during the omitted state. The match is exhaustive by design.
147
-
148
- Source: `docs/packages/api-utils/pages/concepts/result-types.md` Pattern Matching Section
149
-
150
- ### HIGH: Use deprecated state flags (isLoading, isError, isSuccess) instead of AsyncResult state
151
-
152
- ```typescript
153
- // ❌ Wrong: using deprecated flags
154
- const { result, isLoading } = useQuery(...)
155
- if (isLoading.value) {
156
- // Show spinner
157
- } else {
158
- const data = result.value.getValue() // Could be null if isErr!
159
- }
160
- ```
161
-
162
- ```typescript
163
- // ✅ Correct: use AsyncResult state exclusively
164
- const { result } = useQuery(...)
165
- if (result.value.isLoading()) {
166
- // Show spinner
167
- } else if (result.value.isOk()) {
168
- const data = result.value.getValue() // Safe!
169
- }
170
- ```
171
-
172
- `isLoading`, `isError`, and `isSuccess` on `UseQueryReturnType` are deprecated — they exist for backward compatibility but are less type-safe than AsyncResult. Always prefer `result.value.isLoading()`, `result.value.isErr()`, and `result.value.isOk()`.
173
-
174
- Source: `src/composables/query/query.composable.ts` — `UseQueryReturnType` deprecated annotations
175
-
176
99
  ## Next Steps
177
100
 
178
101
  - [Writing Queries](../writing-queries/SKILL.md) — Fetch single resources with caching
@@ -1,43 +1,24 @@
1
1
  ---
2
2
  name: cache-management
3
3
  description: >
4
- Type-safe QueryClient class with get/set/update/invalidate methods, rollback support from update(), predicate-based updates, cascade invalidation strategy, shared cache across components.
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: "1.2.0"
8
- sources:
9
- - "wisemen-digital/wisemen-core:packages/web/api-utils/src/utils/query-client/queryClient.ts"
10
- - "wisemen-digital/wisemen-core:packages/web/api-utils/src/config/config.ts"
11
7
  ---
12
8
 
13
9
  # @wisemen/vue-core-api-utils — Cache Management
14
10
 
15
- Manually read, write, update, and invalidate the query cache using the type-safe `QueryClient` class. This is useful for optimistic updates and strategically invalidating affected queries.
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.
16
12
 
17
13
  ## Setup
18
14
 
19
- The library exports a `QueryClient` class that wraps the underlying TanStack QueryClient with type-safe methods. Create a helper function in your project to get a typed instance:
20
-
21
- ```typescript
22
- // src/api/queryClient.ts
23
-
24
- import { QueryClient, getTanstackQueryClient } from '@wisemen/vue-core-api-utils'
25
- import type { ProjectQueryKeys } from '@/types/queryKey.type'
26
-
27
- export function useQueryClient() {
28
- return new QueryClient<ProjectQueryKeys>(getTanstackQueryClient())
29
- }
30
- ```
31
-
32
- Then use it anywhere in your composables or components:
33
-
34
15
  ```typescript
35
- import { useQueryClient } from '@/api/queryClient'
16
+ import { useQueryClient } from '@/api'
36
17
 
37
18
  const queryClient = useQueryClient()
38
19
 
39
20
  // Get cached data
40
- const contacts = queryClient.get('contactDetail')
21
+ const contact = queryClient.get(['contactDetail', { contactUuid: '123' }])
41
22
 
42
23
  // Set cached data
43
24
  queryClient.set(
@@ -45,16 +26,18 @@ queryClient.set(
45
26
  updatedContact
46
27
  )
47
28
 
48
- // Update cached data with a predicate (returns rollback function)
29
+ // Update cached data with a predicate (returns { rollback } for reverting)
49
30
  const { rollback } = queryClient.update('contactList', {
50
31
  by: (contact) => contact.id === '123',
51
32
  value: (contact) => ({ ...contact, name: 'Updated' }),
52
33
  })
53
34
 
54
- // Invalidate queries (trigger refetch)
35
+ // Invalidate queries (async — triggers refetch)
55
36
  await queryClient.invalidate('contactList')
56
37
  ```
57
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
+
58
41
  ## Core Patterns
59
42
 
60
43
  ### Get cached data
@@ -62,34 +45,38 @@ await queryClient.invalidate('contactList')
62
45
  ```typescript
63
46
  const queryClient = useQueryClient()
64
47
 
65
- // Get all queries with a key (returns array)
66
- const allContacts = queryClient.get('contactDetail')
48
+ // Get specific query
49
+ const contact = queryClient.get(
50
+ ['contactDetail', { contactUuid: '123' }]
51
+ )
67
52
 
68
- // Get exact query stored as ['contactDetail']
69
- const exactContact = queryClient.get('contactDetail', { isExact: true })
53
+ // Get all queries with a key
54
+ const allContacts = queryClient.get('contactList')
70
55
 
71
- // Get specific query with params (returns single item or null)
72
- const contact = queryClient.get(['contactDetail', { contactUuid: '123' }] as const)
56
+ // Get exact query only
57
+ const specificQuery = queryClient.get('contactList', { isExact: true })
73
58
  ```
74
59
 
75
- Returns the cached entity or null if not cached. The QueryClient infers the entity type from your query key definition.
60
+ Returns the cached data or null if not cached. The QueryClient infers entity type from your query key definition.
76
61
 
77
62
  ### Set cached data
78
63
 
79
64
  ```typescript
80
65
  const queryClient = useQueryClient()
81
66
 
82
- // Set query with key + params
83
67
  queryClient.set(
84
68
  ['contactDetail', { contactUuid: '123' }],
85
- { uuid: '123', name: 'John', email: 'john@email.com' }
69
+ { id: '123', name: 'John', email: 'john@email.com' }
86
70
  )
87
71
 
88
- // Set query with just the key (stores as ['contactDetail'])
89
- queryClient.set('contactDetail', { uuid: '123', name: 'John', email: 'john@email.com' })
72
+ // For lists, set works with arrays too
73
+ queryClient.set('contactList', [
74
+ { id: '123', name: 'John' },
75
+ { id: '456', name: 'Jane' },
76
+ ])
90
77
  ```
91
78
 
92
- `set()` replaces all cached data for that specific query key.
79
+ `set()` replaces all cached data for that query key.
93
80
 
94
81
  ### Update cached data with predicates
95
82
 
@@ -105,25 +92,14 @@ const { rollback } = queryClient.update('contactList', {
105
92
  }),
106
93
  })
107
94
 
108
- // If the operation should be reverted (e.g. mutation failed):
109
- rollback()
110
- ```
111
-
112
- `update()` returns a `{ rollback }` function that reverts the cache to its state before the update. Safe to call multiple times (subsequent calls are no-ops).
113
-
114
- ### Update specific query with params tuple
115
-
116
- ```typescript
117
- const queryClient = useQueryClient()
118
-
119
- // Update only the specific cached query for this contact
120
- queryClient.update(['contactDetail', { contactUuid: '123' }] as const, {
121
- by: () => true, // Single entity — always matches
95
+ // For single entities, the predicate always matches
96
+ const { rollback: rollbackDetail } = queryClient.update('contactDetail', {
97
+ by: (contact) => true,
122
98
  value: (contact) => ({ ...contact, name: 'Updated' }),
123
99
  })
124
100
  ```
125
101
 
126
- Using a key+params tuple updates only the specific query, not all queries with that key.
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.
127
103
 
128
104
  ### Invalidate and refetch
129
105
 
@@ -133,105 +109,28 @@ const queryClient = useQueryClient()
133
109
  // Invalidate all queries with this key
134
110
  await queryClient.invalidate('contactList')
135
111
 
136
- // Invalidate specific query with params
137
- await queryClient.invalidate(['contactDetail', { contactUuid: '123' }] as const)
112
+ // Invalidate specific query
113
+ await queryClient.invalidate(['contactDetail', { contactUuid: '123' }])
138
114
 
139
115
  // After invalidation, the next query interaction triggers a refetch
140
116
  ```
141
117
 
142
118
  Invalidation marks cached data as stale. The next interaction (component mount, user action) triggers a refetch.
143
119
 
144
- ## Common Mistakes
145
-
146
- ### HIGH: Create QueryClient without typed query keys; lose type safety
147
-
148
- ```typescript
149
- // ❌ Wrong: untyped QueryClient
150
- import { QueryClient, getTanstackQueryClient } from '@wisemen/vue-core-api-utils'
151
-
152
- const queryClient = new QueryClient(getTanstackQueryClient())
153
- // All methods fall back to `object` for query keys — no autocomplete, no type checking
154
- ```
155
-
156
- ```typescript
157
- // ✅ Correct: typed QueryClient
158
- import { QueryClient, getTanstackQueryClient } from '@wisemen/vue-core-api-utils'
159
- import type { ProjectQueryKeys } from '@/types/queryKey.type'
160
-
161
- const queryClient = new QueryClient<ProjectQueryKeys>(getTanstackQueryClient())
162
- // queryClient.get('nonExistentKey') → TypeScript error
163
- // queryClient.update('contactList', { by: (c) => c.nonExistentField }) → TypeScript error
164
- ```
165
-
166
- Always provide your `ProjectQueryKeys` type generic when instantiating QueryClient.
167
-
168
- Source: `src/utils/query-client/queryClient.ts` — `QueryClient<TQueryKeys>` class
169
-
170
- ### HIGH: Discard the rollback return from update(); can't revert optimistic changes
171
-
172
- ```typescript
173
- // ❌ Wrong: ignoring rollback
174
- queryClient.update('contactList', {
175
- by: (c) => c.id === '123',
176
- value: (c) => ({ ...c, name: 'Updated' }),
177
- })
178
- // No way to undo if the mutation fails
179
- ```
180
-
181
- ```typescript
182
- // ✅ Correct: capture rollback for error recovery
183
- const { rollback } = queryClient.update('contactList', {
184
- by: (c) => c.id === '123',
185
- value: (c) => ({ ...c, name: 'Updated' }),
186
- })
187
-
188
- const result = await execute(formData)
189
- if (result.isErr()) {
190
- rollback() // Reverts the cache to its pre-update state
191
- }
192
- ```
193
-
194
- `update()` returns `{ rollback }` — always capture it when doing optimistic updates so you can revert on error.
195
-
196
- Source: `src/utils/query-client/queryClient.ts` — `QueryClientUpdateResult`
197
-
198
- ### MEDIUM: Call set() without rollback plan; UI flashes stale data on error
199
-
200
- ```typescript
201
- // ❌ Wrong: immediate set without error recovery
202
- queryClient.set(['contactDetail', { contactUuid }], newData)
203
- // Cache updated but if the mutation fails, data is wrong with no way to revert
204
- ```
205
-
206
- ```typescript
207
- // ✅ Correct: prefer update() which has built-in rollback
208
- const { rollback } = queryClient.update(['contactDetail', { contactUuid }] as const, {
209
- by: () => true,
210
- value: () => newData,
211
- })
212
-
213
- const result = await execute(formData)
214
- if (result.isErr()) {
215
- rollback()
216
- }
217
- ```
218
-
219
- Prefer `update()` over `set()` for optimistic changes because `update()` automatically captures the previous state for rollback.
220
-
221
- Source: `src/utils/query-client/queryClient.ts`
222
-
223
120
  ## Cache Strategy
224
121
 
225
122
  > Explicitly invalidate only the queries affected by the mutation. Let lazy refetch handle the rest when users navigate to pages needing other data.
123
+ >
124
+ > — Maintainer guidance
226
125
 
227
126
  When a mutation succeeds, look at what changed:
228
127
  - If you updated a contact, invalidate `contactDetail` and `contactList` (they both show that contact)
229
- - If you archived a conversation, invalidate `conversationList` (but maybe not `conversationDetail` unless it's the one you archived)
128
+ - If you archived a conversation, invalidate `conversationList` (but maybe not `conversationDetail` unless showing the one you archived)
230
129
  - Don't invalidate unrelated queries — let them refetch lazily when needed
231
130
 
232
131
  ## Shared Cache Across Components
233
132
 
234
- Multiple components using the same query key share the same cached data. This is a feature, not a bug.
133
+ Important: Multiple components using the same query key share the same cached data. This is a feature, not a bug.
235
134
 
236
135
  ```typescript
237
136
  // ComponentA
@@ -246,8 +145,8 @@ const { result: resultB } = useQuery('userDetail', {
246
145
  queryFn: () => UserService.getById('same-id'),
247
146
  })
248
147
 
249
- // resultA and resultB share the SAME cached value
250
- // Mutation in B that invalidates userDetail also triggers a refetch in A
148
+ // resultA and resultB are the SAME cached value
149
+ // Mutation in B invalidates A's cache
251
150
  ```
252
151
 
253
152
  Use this to your advantage: invalidate a query and all components using it refetch automatically.
@@ -256,4 +155,3 @@ Use this to your advantage: invalidate a query and all components using it refet
256
155
 
257
156
  - [Writing Mutations](../writing-mutations/SKILL.md) — Every mutation needs to know which queries to invalidate
258
157
  - [Writing Queries](../writing-queries/SKILL.md) — Understanding caching strategy informs cache management choices
259
- - [Optimistic UIs](../optimistic-uis/SKILL.md) — Full optimistic update pattern using update() and rollback
@@ -4,11 +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: "1.2.0"
8
- sources:
9
- - "wisemen-digital/wisemen-core:packages/web/api-utils/src/async-result/asyncResult.ts"
10
- - "wisemen-digital/wisemen-core:packages/web/api-utils/src/types/apiError.type.ts"
11
- - "wisemen-digital/wisemen-core:packages/web/api-utils/src/config/config.ts"
12
7
  ---
13
8
 
14
9
  # @wisemen/vue-core-api-utils — Foundations
@@ -196,8 +191,8 @@ async function handleRefresh() {
196
191
 
197
192
  // Automatic refetch on mutation (via queryKeysToInvalidate)
198
193
  const { execute } = useMutation({
199
- queryFn: (data) => ContactService.update(data),
200
- queryKeysToInvalidate: { contactDetail: () => true },
194
+ queryFn: ({ body }: { body: ContactUpdateForm }) => ContactService.update(body),
195
+ queryKeysToInvalidate: { contactDetail: {} },
201
196
  })
202
197
  // After execute succeeds, contactDetail is invalidated
203
198
  // Next useQuery('contactDetail') refetches fresh data
@@ -245,8 +240,9 @@ Errors are typed and structured using `neverthrow`:
245
240
  interface ApiExpectedError {
246
241
  errors: Array<{
247
242
  code: string
248
- message: string
249
- details?: unknown
243
+ detail: string
244
+ status: string
245
+ source?: { pointer: string }
250
246
  }>
251
247
  }
252
248
 
@@ -258,10 +254,11 @@ const result = new AsyncResultErr(apiError)
258
254
  result.match({
259
255
  ok: (data) => {}, // not executed
260
256
  err: (error) => {
261
- // error is ApiError
262
- if (error instanceof ApiExpectedError) {
257
+ // error is ApiError — narrow with 'errors' in error
258
+ if ('errors' in error) {
263
259
  // Handle known API errors
264
260
  const codes = error.errors.map(e => e.code)
261
+ const detail = error.errors[0].detail
265
262
  } else {
266
263
  // Handle network/parsing errors
267
264
  console.error(error.message)
@@ -272,154 +269,6 @@ result.match({
272
269
 
273
270
  Error types are defined at library initialization via the generic `TErrorCode`. This ensures type-safe error handling across queries and mutations.
274
271
 
275
- ## Common Mistakes
276
-
277
- ### CRITICAL: Confuse Result (neverthrow) with AsyncResult; treat ok/err as boolean
278
-
279
- ```typescript
280
- // ❌ Wrong: neverthrow Result is not AsyncResult
281
- const result = new Result(contact, null) // This is not how neverthrow works
282
- if (result.ok) { // `.ok` doesn't exist
283
- console.log(result.value)
284
- }
285
-
286
- // Or even worse: treating AsyncResult like a boolean
287
- const { result } = useQuery(...)
288
- if (result.value) {
289
- // This is always true; result is always defined (Loading | Ok | Err)
290
- }
291
- ```
292
-
293
- ```typescript
294
- // ✅ Correct: AsyncResult requires exhaustive pattern matching
295
- const { result } = useQuery('contactDetail', {
296
- queryFn: () => ContactService.getDetail(uuid),
297
- })
298
-
299
- result.value.match({
300
- loading: () => showSpinner(),
301
- ok: (contact) => showContact(contact),
302
- err: (error) => showError(error),
303
- })
304
-
305
- // Or use type guards
306
- if (result.value.isOk()) {
307
- console.log(result.value.getValue())
308
- } else if (result.value.isErr()) {
309
- console.log(result.value.getError())
310
- } else {
311
- showSpinner()
312
- }
313
- ```
314
-
315
- AsyncResult requires explicit handling of all three states. The type system won't let you skip a state.
316
-
317
- Source: `docs/packages/api-utils/pages/concepts/result-types.md`
318
-
319
- ### MEDIUM: Misunderstand staleTime; think data refreshes automatically after staleTime
320
-
321
- ```typescript
322
- // ❌ Wrong: assuming staleTime auto-refetches
323
- const { result } = useQuery('contactDetail', {
324
- queryFn: () => ContactService.getDetail(uuid),
325
- staleTime: 5 * 60 * 1000, // Not an auto-refresh interval
326
- })
327
-
328
- // At T=5m, data doesn't automatically refetch.
329
- // It's just marked stale. Refetch happens on next interaction
330
- // (component mount, user action, mutation invalidation)
331
- ```
332
-
333
- ```typescript
334
- // ✅ Correct: staleTime is a grace period, not an interval
335
- const { result, refetch } = useQuery('contactDetail', {
336
- queryFn: () => ContactService.getDetail(uuid),
337
- staleTime: 5 * 60 * 1000,
338
- })
339
-
340
- // Data is fresh for 5 minutes (instant returns)
341
- // After 5 minutes, next access triggers refetch
342
- // For auto-refresh, use refetch() in a watchEffect or timer
343
-
344
- watchEffect(async () => {
345
- // Refetch every 10 seconds
346
- const interval = setInterval(() => refetch(), 10 * 1000)
347
- onCleanup(() => clearInterval(interval))
348
- })
349
- ```
350
-
351
- 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.
352
-
353
- ### HIGH: Misunderstand gcTime and cache eviction; assume cache persists forever
354
-
355
- ```typescript
356
- // ❌ Wrong: assuming cache is permanent
357
- const { result } = useQuery('contactDetail', {
358
- queryFn: () => ContactService.getDetail(uuid),
359
- gcTime: 5 * 60 * 1000, // Default is 5 minutes
360
- })
361
-
362
- // If component unmounts and user is gone for > 5 minutes,
363
- // Next access refetches fresh (cache is evicted)
364
- // This is correct behavior, but if you expected cached data...
365
- ```
366
-
367
- ```typescript
368
- // ✅ Correct: increase gcTime if you want longer-lived cache
369
- const { result } = useQuery('contactDetail', {
370
- queryFn: () => ContactService.getDetail(uuid),
371
- staleTime: 5 * 60 * 1000, // Fresh for 5 minutes
372
- gcTime: 60 * 60 * 1000, // Keep in memory for 1 hour
373
- })
374
-
375
- // After 5 minutes (stale) but before 1 hour (gc),
376
- // Returning cached data (but trigger refetch automatically)
377
- // After 1 hour, cache is deleted; next access refetches fresh
378
- ```
379
-
380
- 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.
381
-
382
- Source: `packages/web/api-utils/src/config/config.ts`
383
-
384
- ### MEDIUM: Forget that QueryClient is shared; one query invalidation affects all components
385
-
386
- ```typescript
387
- // ❌ Wrong: not realizing cache is global
388
- const { execute } = useMutation({
389
- queryFn: () => ContactService.update(data),
390
- queryKeysToInvalidate: { contactDetail: () => true },
391
- })
392
-
393
- // Component A: detail view
394
- // Component B: list view (also uses contactDetail)
395
- // Even though A only updated one contact,
396
- // B's cache is invalidated too (because same query key)
397
- ```
398
-
399
- ```typescript
400
- // ✅ Correct: QueryClient is intentionally shared
401
- export function useContactMutation() {
402
- const { execute } = useMutation({
403
- queryFn: () => ContactService.update(data),
404
- queryKeysToInvalidate: {
405
- // Invalidates all components using this key
406
- contactDetail: () => true,
407
- // Also invalidate lists that show this contact
408
- contactList: () => true,
409
- },
410
- })
411
-
412
- return { execute }
413
- }
414
-
415
- // When A updates a contact, both A and B refetch.
416
- // This is the intended design: shared cache across app.
417
- ```
418
-
419
- QueryClient is application-wide (singleton). Invalidating a query invalidates for all components using that key. This is a feature: synchronized cache across the app.
420
-
421
- Source: `packages/web/api-utils/src/utils/query-client/queryClient.ts`
422
-
423
272
  ## Integration pattern
424
273
 
425
274
  The full integration: