@gallopsystems/agent-skills 1.5.0 → 1.6.0
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/README.md +2 -1
- package/package.json +1 -1
- package/plugins/nuxt-nitro-api/.claude-plugin/plugin.json +1 -1
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/SKILL.md +23 -8
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/auth-patterns.md +1 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/caching.md +68 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/composables-utils.md +31 -1
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/error-handling.md +74 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/fetch-patterns.md +18 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/formatters.md +11 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/layers.md +40 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/page-structure.md +1 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/route-rules.md +41 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/server-runtime.md +97 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/ssr-client.md +60 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/state-management.md +68 -0
- package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/storage.md +59 -0
- package/plugins/volt-primevue/skills/volt-primevue/SKILL.md +56 -12
- package/plugins/volt-primevue/skills/volt-primevue/config.md +51 -0
- package/plugins/volt-primevue/skills/volt-primevue/gotchas.md +49 -0
- package/plugins/volt-primevue/skills/volt-primevue/theming.md +20 -6
- package/plugins/vue-nuxt/.claude-plugin/plugin.json +8 -0
- package/plugins/vue-nuxt/skills/vue-nuxt/SKILL.md +48 -0
- package/plugins/vue-nuxt/skills/vue-nuxt/auto-imports.md +48 -0
- package/plugins/vue-nuxt/skills/vue-nuxt/component-authoring.md +159 -0
- package/plugins/vue-nuxt/skills/vue-nuxt/composables.md +95 -0
- package/plugins/vue-nuxt/skills/vue-nuxt/reactivity.md +133 -0
- package/plugins/vue-nuxt/skills/vue-nuxt/slots.md +139 -0
- package/plugins/vue-nuxt/skills/vue-nuxt/template-idioms.md +142 -0
- package/plugins/vue-nuxt/skills/vue-nuxt/v-model.md +106 -0
- package/plugins/vue-nuxt/skills/vue-nuxt/watch.md +194 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# `watch` — the escape hatch, not the default
|
|
2
|
+
|
|
3
|
+
`watch` reactively performs **side effects**. It is not for deriving values —
|
|
4
|
+
that's `computed`. Heavy `watch` use is usually a `computed`, a *writable*
|
|
5
|
+
`computed`, a `defineModel`, or an event handler in disguise.
|
|
6
|
+
|
|
7
|
+
**Rule of thumb:** read the watcher body. If it ends by assigning one reactive
|
|
8
|
+
value from others and nothing leaves Vue's world (no fetch, DOM, third-party lib,
|
|
9
|
+
storage, URL), delete it and write a `computed`.
|
|
10
|
+
|
|
11
|
+
> Grounded in a 159-watcher audit across 7 Nuxt apps: **77% were legitimate, ~9%
|
|
12
|
+
> clear smells, ~14% borderline.** The smells clustered into the four shapes
|
|
13
|
+
> below; over half the *borderline* cases were one pattern ("reset a field in a
|
|
14
|
+
> watcher"). The legit cases are real — the trap is using `watch` for what the
|
|
15
|
+
> framework already does declaratively.
|
|
16
|
+
|
|
17
|
+
## Writing a value back? Use a writable `computed`, not a watch
|
|
18
|
+
|
|
19
|
+
The most common reason people reach for `watch` is to *write a value back* —
|
|
20
|
+
mirror a prop, transform a bound value, proxy a child's `v-model`. A `computed`
|
|
21
|
+
can have a setter; use it.
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
// ❌ two watches mirroring a prop both ways — desyncs, pure boilerplate
|
|
25
|
+
const visible = ref(props.visible)
|
|
26
|
+
watch(() => props.visible, (v) => (visible.value = v))
|
|
27
|
+
watch(visible, (v) => emit('update:visible', v))
|
|
28
|
+
|
|
29
|
+
// ✅ writable computed — one source of truth, genuinely two-way
|
|
30
|
+
const visible = computed({
|
|
31
|
+
get: () => props.visible,
|
|
32
|
+
set: (v) => emit('update:visible', v),
|
|
33
|
+
})
|
|
34
|
+
// ✅ or, for a component's OWN model: const visible = defineModel<boolean>('visible')
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
A writable `computed` also transforms a value across the boundary (string ↔ Date):
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
const date = computed({
|
|
41
|
+
get: () => parseISO(model.value),
|
|
42
|
+
set: (d) => (model.value = d.toISOString()),
|
|
43
|
+
})
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
(Both getter and setter receive the previous value as their first argument if you
|
|
47
|
+
need it.)
|
|
48
|
+
|
|
49
|
+
Two rules from the Vue docs that keep this correct:
|
|
50
|
+
|
|
51
|
+
- **Getters must be side-effect-free.** A `computed` getter only derives and
|
|
52
|
+
returns — no mutating other state, no async, no DOM. Side effects *in reaction
|
|
53
|
+
to* a change are what `watch` is for.
|
|
54
|
+
- **A computed value is a read-only snapshot.** Never mutate what a `computed`
|
|
55
|
+
returns; update the *source* state it derives from and let a new snapshot be
|
|
56
|
+
produced.
|
|
57
|
+
|
|
58
|
+
## When `watch` IS the right tool
|
|
59
|
+
|
|
60
|
+
Only when the effect crosses **out of** the reactive graph:
|
|
61
|
+
|
|
62
|
+
- **Fetch on a changing key** — prefer a reactive `useFetch` `query`/key (it
|
|
63
|
+
auto-refetches; see `nuxt-nitro-api/fetch-patterns.md`). Reach for `watch` only
|
|
64
|
+
for imperative `$fetch` keyed off a single id.
|
|
65
|
+
- **Drive a non-Vue library** — Tiptap, a Leaflet/Google map, a signature canvas,
|
|
66
|
+
a PrimeVue popover you `.hide()` imperatively.
|
|
67
|
+
- **DOM / timers** — `document.body.style.overflow`, a debounce `setTimeout`, a
|
|
68
|
+
polling `setInterval` (clear it in `onUnmounted`).
|
|
69
|
+
- **Persist** — a `localStorage`/`useCookie` write, debounced auto-save of a
|
|
70
|
+
deep-watched form.
|
|
71
|
+
- **URL sync** — `router.replace({ query: { ...route.query, tab } })`.
|
|
72
|
+
- **Re-seed local state on dialog open** — `watch(visible, (v) => { if (v) initForm() })`.
|
|
73
|
+
The single most common legit pattern.
|
|
74
|
+
- **Clone a server prop into a locally-editable draft** —
|
|
75
|
+
`watch(() => props.record, (r) => { if (r) form.value = structuredClone(toRaw(r)) }, { immediate: true })`.
|
|
76
|
+
|
|
77
|
+
### Cancel stale work with `onWatcherCleanup`
|
|
78
|
+
|
|
79
|
+
A watcher that starts async work (a keyed `$fetch`, a timer) must cancel the
|
|
80
|
+
previous run, or out-of-order responses clobber state — the #1 correctness bug in
|
|
81
|
+
keyed-fetch watchers. Vue 3.5's `onWatcherCleanup(fn)` registers teardown that
|
|
82
|
+
runs before the next fire and on stop:
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
import { onWatcherCleanup } from 'vue'
|
|
86
|
+
|
|
87
|
+
watch(query, async (q) => {
|
|
88
|
+
const ctrl = new AbortController()
|
|
89
|
+
onWatcherCleanup(() => ctrl.abort()) // abort the in-flight request if q changes again
|
|
90
|
+
results.value = await $fetch('/api/search', { query: { q }, signal: ctrl.signal })
|
|
91
|
+
})
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Gotcha: `onWatcherCleanup` only registers **synchronously, before the first
|
|
95
|
+
`await`**. For teardown decided after an await, use the callback's third arg
|
|
96
|
+
instead — `watch(src, (v, _old, onCleanup) => { … onCleanup(fn) })`.
|
|
97
|
+
|
|
98
|
+
### `flush: 'post'` runs the callback after the DOM patches
|
|
99
|
+
|
|
100
|
+
When a watcher reacts to a change by reading/scrolling the **updated** DOM, give
|
|
101
|
+
it `{ flush: 'post' }` so it runs after Vue patches — no manual `await nextTick()`.
|
|
102
|
+
They're ~one microtask apart and interchangeable; pick by clarity (prefer
|
|
103
|
+
`flush: 'post'` for an effect that *reacts to reactive change*, `nextTick` for a
|
|
104
|
+
one-shot after an imperative toggle).
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
watch(activeId, () => scrollActiveIntoView(), { flush: 'post' }) // DOM already updated
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Smell catalog (with refactors)
|
|
111
|
+
|
|
112
|
+
| Smell | Tell | Reach for |
|
|
113
|
+
|---|---|---|
|
|
114
|
+
| **derive-state** | body assigns a filtered/mapped view into another ref | `computed` |
|
|
115
|
+
| **prop-sync** | local `ref(props.x)` kept in sync by a watch (often + a 2nd watch emitting back) | `defineModel` or a writable `computed` |
|
|
116
|
+
| **side-effect-in-handler** | watching a value that only changes via one control, to clear a dependent field | that control's `@update:model-value` handler |
|
|
117
|
+
| **manual-refetch** | watching filter refs to call a function that calls `useFetch` | a `computed` `query` passed to `useFetch` |
|
|
118
|
+
|
|
119
|
+
The biggest real cluster was **side-effect-in-handler** — resetting dependent
|
|
120
|
+
fields when a Select changed. In a watcher it hides cause/effect and re-fires on
|
|
121
|
+
programmatic form reseeds; the colocated handler is direct and only fires on the
|
|
122
|
+
user action:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
// ❌ watch(() => form.entityType, () => { form.eventType = ''; form.conditionValue = null })
|
|
126
|
+
// ✅
|
|
127
|
+
function onEntityType(v) { form.entityType = v; form.eventType = ''; form.conditionValue = null }
|
|
128
|
+
// <Select v-model="form.entityType" @update:model-value="onEntityType" />
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Watch for **cascades** — a chain of watches each resetting the next (entityType →
|
|
132
|
+
eventType → conditionProperty) is just a chain of handlers, far harder to follow.
|
|
133
|
+
|
|
134
|
+
**Borderline tell — `seed-via-immediate`:** `watch(asyncList, (l) => { if (!local.value) local.value = pick(l) }, { immediate: true })` to default a *user-mutable* ref from fetched data is defensible (a pure `computed` can't be user-overridden) — but keep the `!local.value` guard so a refetch doesn't clobber an in-progress edit.
|
|
135
|
+
|
|
136
|
+
## `deep: true` is expensive — use it sparingly
|
|
137
|
+
|
|
138
|
+
A deep watch traverses **every** nested property of the watched object on each
|
|
139
|
+
check, so it gets costly on large structures. Before reaching for it:
|
|
140
|
+
|
|
141
|
+
- **Watch a getter of the specific field** instead of the whole object:
|
|
142
|
+
`watch(() => form.address.zip, ...)` rather than `watch(form, ..., { deep: true })`.
|
|
143
|
+
- If you genuinely need to react to *any* field of a form (e.g. debounced
|
|
144
|
+
auto-save), a deep watch is legitimate — but scope it as tightly as you can.
|
|
145
|
+
- For deriving from a few keys of a nested object, a `computed` (or `watchEffect`)
|
|
146
|
+
tracks **only the keys actually read**, avoiding the full traversal a deep watch
|
|
147
|
+
pays for.
|
|
148
|
+
|
|
149
|
+
**Deep-watch aliasing trap:** in a deep watch the callback's `oldValue` and
|
|
150
|
+
`newValue` are the **same reference** (Vue mutated the object in place), so you
|
|
151
|
+
*cannot* diff old-vs-new inside the callback — `if (next.x !== prev.x)` is always
|
|
152
|
+
false. Watch a derived getter instead, so old/new are distinct primitives:
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
// ❌ watch(items, (next, prev) => { if (next.length !== prev.length) … }, { deep: true }) // never fires
|
|
156
|
+
// ✅ watch(() => items.value.length, (next, prev) => { if (next > prev) onAdded() })
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## `watch` vs `watchEffect`
|
|
160
|
+
|
|
161
|
+
Both run side effects reactively; they differ in how they **track dependencies**:
|
|
162
|
+
|
|
163
|
+
- **`watch(source, cb)`** tracks ONLY the explicit source, fires lazily and only
|
|
164
|
+
when it *actually changed*, and hands you **old + new** values. Use it when you
|
|
165
|
+
want precise control over what fires the effect, need the previous value, or
|
|
166
|
+
want it lazy.
|
|
167
|
+
- **`watchEffect(cb)`** runs immediately and auto-tracks every reactive property
|
|
168
|
+
read during its **synchronous** execution. Terser, and it removes the burden of
|
|
169
|
+
maintaining a source list — genuinely better for a side effect with **several**
|
|
170
|
+
dependencies, or one reading a few keys of a nested object (it tracks only
|
|
171
|
+
what's used, unlike a `deep` watch).
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
// watch: todoId named twice (source + inside callback)
|
|
175
|
+
watch(todoId, async () => { data.value = await $fetch(`/api/todos/${todoId.value}`) }, { immediate: true })
|
|
176
|
+
// watchEffect: todoId.value is both the read and the tracked dep — no immediate flag, no repeated source
|
|
177
|
+
watchEffect(async () => { data.value = await $fetch(`/api/todos/${todoId.value}`) })
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Two cautions on `watchEffect`:
|
|
181
|
+
|
|
182
|
+
- **Async tracking gotcha:** it only tracks deps accessed **before the first
|
|
183
|
+
`await`**. Read a ref *after* an `await` and it silently won't re-trigger on
|
|
184
|
+
that ref. With multiple async deps, read them all up front or use an explicit
|
|
185
|
+
`watch`.
|
|
186
|
+
- **It tempts the derive-state smell.** Because it runs immediately and
|
|
187
|
+
auto-tracks, `watchEffect` is the most common home for "compute and assign" —
|
|
188
|
+
which is a `computed`. The *only* `watchEffect` smell in the audit was exactly
|
|
189
|
+
this: `watchEffect(() => { orgField.options = organizations.value })` → should
|
|
190
|
+
be a `computed` that maps the field list.
|
|
191
|
+
|
|
192
|
+
Default: **`computed` for derivation, `watch` for a precise single-source effect,
|
|
193
|
+
`watchEffect` for a genuine multi-dependency side effect** whose deps are all read
|
|
194
|
+
synchronously.
|