@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
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
6
|
"type": "module",
|
|
7
|
-
"version": "2.0.
|
|
7
|
+
"version": "2.0.2",
|
|
8
8
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
@@ -33,9 +33,10 @@
|
|
|
33
33
|
"skills"
|
|
34
34
|
],
|
|
35
35
|
"peerDependencies": {
|
|
36
|
-
"@tanstack/vue-query": "
|
|
37
|
-
"neverthrow": "
|
|
38
|
-
"
|
|
36
|
+
"@tanstack/vue-query": "^5.100.6",
|
|
37
|
+
"neverthrow": "^8.2.0",
|
|
38
|
+
"zod": "^4.3.5",
|
|
39
|
+
"vue": "^3.5.27"
|
|
39
40
|
},
|
|
40
41
|
"devDependencies": {
|
|
41
42
|
"@tanstack/intent": "^0.0.29",
|
|
@@ -45,7 +46,7 @@
|
|
|
45
46
|
"typescript": "5.9.3",
|
|
46
47
|
"vue": "3.5.27",
|
|
47
48
|
"vitest": "4.1.0",
|
|
48
|
-
"@wisemen/eslint-config-vue": "2.1.
|
|
49
|
+
"@wisemen/eslint-config-vue": "2.1.4"
|
|
49
50
|
},
|
|
50
51
|
"scripts": {
|
|
51
52
|
"build": "tsdown",
|
|
@@ -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
|
|
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`
|
|
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
|
|
16
|
+
import { useQueryClient } from '@/api'
|
|
36
17
|
|
|
37
18
|
const queryClient = useQueryClient()
|
|
38
19
|
|
|
39
20
|
// Get cached data
|
|
40
|
-
const
|
|
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
|
|
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 (
|
|
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
|
|
66
|
-
const
|
|
48
|
+
// Get specific query
|
|
49
|
+
const contact = queryClient.get(
|
|
50
|
+
['contactDetail', { contactUuid: '123' }]
|
|
51
|
+
)
|
|
67
52
|
|
|
68
|
-
// Get
|
|
69
|
-
const
|
|
53
|
+
// Get all queries with a key
|
|
54
|
+
const allContacts = queryClient.get('contactList')
|
|
70
55
|
|
|
71
|
-
// Get
|
|
72
|
-
const
|
|
56
|
+
// Get exact query only
|
|
57
|
+
const specificQuery = queryClient.get('contactList', { isExact: true })
|
|
73
58
|
```
|
|
74
59
|
|
|
75
|
-
Returns the cached
|
|
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
|
-
{
|
|
69
|
+
{ id: '123', name: 'John', email: 'john@email.com' }
|
|
86
70
|
)
|
|
87
71
|
|
|
88
|
-
//
|
|
89
|
-
queryClient.set('
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
137
|
-
await queryClient.invalidate(['contactDetail', { contactUuid: '123' }]
|
|
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
|
|
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
|
|
250
|
-
// Mutation in B
|
|
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: (
|
|
200
|
-
queryKeysToInvalidate: { contactDetail:
|
|
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
|
-
|
|
249
|
-
|
|
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 (
|
|
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:
|