mdk-skills 2.1.3
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/.claude/.install.log +4 -0
- package/.claude/settings.json +64 -0
- package/.claude/settings.local.json +7 -0
- package/.claude/skills/agentation/.meta.json +6 -0
- package/.claude/skills/agentation/SKILL.md +107 -0
- package/.claude/skills/fe-biz-patterns/.meta.json +6 -0
- package/.claude/skills/fe-biz-patterns/SKILL.md +26 -0
- package/.claude/skills/fe-biz-patterns/references/infinite-scroll.md +292 -0
- package/.claude/skills/fe-biz-patterns/references/pinia-store.md +174 -0
- package/.claude/skills/fe-biz-patterns/references/service-layer.md +198 -0
- package/.claude/skills/fe-biz-patterns/references/tab-anchor.md +1125 -0
- package/.claude/skills/fe-biz-patterns/references/use-loading.md +114 -0
- package/.claude/skills/frontend-code-review/.meta.json +6 -0
- package/.claude/skills/frontend-code-review/SKILL.md +167 -0
- package/.claude/skills/frontend-code-review/references/checklist.md +298 -0
- package/.claude/skills/frontend-design/.meta.json +6 -0
- package/.claude/skills/frontend-design/LICENSE.txt +177 -0
- package/.claude/skills/frontend-design/SKILL.md +42 -0
- package/.claude/skills/moai-framework-electron/.meta.json +6 -0
- package/.claude/skills/moai-framework-electron/SKILL.md +328 -0
- package/.claude/skills/skill-creator/.meta.json +6 -0
- package/.claude/skills/skill-creator/SKILL.md +356 -0
- package/.claude/skills/skill-creator/references/output-patterns.md +82 -0
- package/.claude/skills/skill-creator/references/workflows.md +28 -0
- package/.claude/skills/skill-creator/scripts/init_skill.py +303 -0
- package/.claude/skills/skill-creator/scripts/package_skill.py +110 -0
- package/.claude/skills/skill-creator/scripts/quick_validate.py +95 -0
- package/.claude/skills/ui-ux-pro-max/.meta.json +6 -0
- package/.claude/skills/ui-ux-pro-max/SKILL.md +228 -0
- package/.claude/skills/ui-ux-pro-max/data/charts.csv +26 -0
- package/.claude/skills/ui-ux-pro-max/data/colors.csv +97 -0
- package/.claude/skills/ui-ux-pro-max/data/landing.csv +31 -0
- package/.claude/skills/ui-ux-pro-max/data/products.csv +97 -0
- package/.claude/skills/ui-ux-pro-max/data/prompts.csv +24 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
- package/.claude/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
- package/.claude/skills/ui-ux-pro-max/data/styles.csv +59 -0
- package/.claude/skills/ui-ux-pro-max/data/typography.csv +58 -0
- package/.claude/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
- package/.claude/skills/ui-ux-pro-max/scripts/core.py +238 -0
- package/.claude/skills/ui-ux-pro-max/scripts/search.py +61 -0
- package/.claude/skills/vue/.meta.json +6 -0
- package/.claude/skills/vue/SKILL.md +103 -0
- package/.claude/skills/vue/references/components.md +323 -0
- package/.claude/skills/vue/references/composables.md +358 -0
- package/.claude/skills/vue/references/directives.md +225 -0
- package/.claude/skills/vue/references/gotchas.md +438 -0
- package/.claude/skills/vue/references/provide-inject.md +174 -0
- package/.claude/skills/vue/references/reactivity.md +289 -0
- package/.claude/skills/vue/references/router.md +181 -0
- package/.claude/skills/vue/references/testing.md +294 -0
- package/.claude/skills/vue/references/typescript.md +172 -0
- package/.claude/skills/vue/references/utils-client.md +156 -0
- package/CLAUDE.md +131 -0
- package/package.json +23 -0
- package/scripts/cli.js +260 -0
- package/scripts/copy-skills.js +86 -0
- package/scripts/core.js +256 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Custom Directives
|
|
3
|
+
description: Create reusable directives for low-level DOM manipulation
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Custom Directives
|
|
7
|
+
|
|
8
|
+
Custom directives provide low-level DOM access for reusable behavior.
|
|
9
|
+
|
|
10
|
+
## When to Use
|
|
11
|
+
|
|
12
|
+
Use custom directives when:
|
|
13
|
+
|
|
14
|
+
- You need direct DOM manipulation
|
|
15
|
+
- The behavior can't be achieved with components or composables
|
|
16
|
+
- You need to apply behavior to native elements
|
|
17
|
+
|
|
18
|
+
## Basic Example
|
|
19
|
+
|
|
20
|
+
```vue
|
|
21
|
+
<script setup lang="ts">
|
|
22
|
+
// v-focus directive
|
|
23
|
+
const vFocus = {
|
|
24
|
+
mounted: (el: HTMLElement) => el.focus()
|
|
25
|
+
}
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<template>
|
|
29
|
+
<input v-focus />
|
|
30
|
+
</template>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Directive Hooks
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
const myDirective = {
|
|
37
|
+
// Before element attributes/listeners are applied
|
|
38
|
+
created(el, binding, vnode) {},
|
|
39
|
+
|
|
40
|
+
// Before element is inserted into DOM
|
|
41
|
+
beforeMount(el, binding, vnode) {},
|
|
42
|
+
|
|
43
|
+
// After element and children are mounted
|
|
44
|
+
mounted(el, binding, vnode) {},
|
|
45
|
+
|
|
46
|
+
// Before parent component updates
|
|
47
|
+
beforeUpdate(el, binding, vnode, prevVnode) {},
|
|
48
|
+
|
|
49
|
+
// After parent component updates
|
|
50
|
+
updated(el, binding, vnode, prevVnode) {},
|
|
51
|
+
|
|
52
|
+
// Before parent component unmounts
|
|
53
|
+
beforeUnmount(el, binding, vnode) {},
|
|
54
|
+
|
|
55
|
+
// After parent component unmounts
|
|
56
|
+
unmounted(el, binding, vnode) {}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Hook Arguments
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
interface DirectiveBinding<T = any> {
|
|
64
|
+
value: T // v-my-dir="value"
|
|
65
|
+
oldValue: T // Previous value (beforeUpdate/updated only)
|
|
66
|
+
arg?: string // v-my-dir:arg
|
|
67
|
+
modifiers: Record<string, boolean> // v-my-dir.foo.bar → { foo: true, bar: true }
|
|
68
|
+
instance: ComponentPublicInstance // Component using the directive
|
|
69
|
+
dir: ObjectDirective // Directive definition object
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Example usage:
|
|
74
|
+
|
|
75
|
+
```vue-html
|
|
76
|
+
<div v-example:foo.bar="baz">
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
// binding object:
|
|
81
|
+
{
|
|
82
|
+
arg: 'foo',
|
|
83
|
+
modifiers: { bar: true },
|
|
84
|
+
value: /* value of baz */,
|
|
85
|
+
oldValue: /* previous value */
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Function Shorthand
|
|
90
|
+
|
|
91
|
+
When you only need `mounted` and `updated` with same behavior:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
// Full form
|
|
95
|
+
const vColor = {
|
|
96
|
+
mounted(el, binding) {
|
|
97
|
+
el.style.color = binding.value
|
|
98
|
+
},
|
|
99
|
+
updated(el, binding) {
|
|
100
|
+
el.style.color = binding.value
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Shorthand (same behavior)
|
|
105
|
+
const vColor = (el: HTMLElement, binding: DirectiveBinding<string>) => {
|
|
106
|
+
el.style.color = binding.value
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Global Registration
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
// main.ts
|
|
114
|
+
const app = createApp(App)
|
|
115
|
+
|
|
116
|
+
app.directive('focus', {
|
|
117
|
+
mounted: (el) => el.focus()
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
// Shorthand
|
|
121
|
+
app.directive('color', (el, binding) => {
|
|
122
|
+
el.style.color = binding.value
|
|
123
|
+
})
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Object Literals
|
|
127
|
+
|
|
128
|
+
Pass multiple values:
|
|
129
|
+
|
|
130
|
+
```vue-html
|
|
131
|
+
<div v-demo="{ color: 'white', text: 'hello' }">
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
const vDemo = (el: HTMLElement, binding: DirectiveBinding<{ color: string; text: string }>) => {
|
|
136
|
+
console.log(binding.value.color) // 'white'
|
|
137
|
+
console.log(binding.value.text) // 'hello'
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Dynamic Arguments
|
|
142
|
+
|
|
143
|
+
```vue-html
|
|
144
|
+
<div v-my-directive:[dynamicArg]="value">
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Practical Examples
|
|
148
|
+
|
|
149
|
+
### v-click-outside
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
const vClickOutside = {
|
|
153
|
+
mounted(el: HTMLElement, binding: DirectiveBinding<() => void>) {
|
|
154
|
+
el._clickOutside = (event: MouseEvent) => {
|
|
155
|
+
if (!el.contains(event.target as Node)) {
|
|
156
|
+
binding.value()
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
document.addEventListener('click', el._clickOutside)
|
|
160
|
+
},
|
|
161
|
+
unmounted(el: HTMLElement) {
|
|
162
|
+
document.removeEventListener('click', el._clickOutside)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### v-tooltip
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
const vTooltip = {
|
|
171
|
+
mounted(el: HTMLElement, binding: DirectiveBinding<string>) {
|
|
172
|
+
el.setAttribute('title', binding.value)
|
|
173
|
+
},
|
|
174
|
+
updated(el: HTMLElement, binding: DirectiveBinding<string>) {
|
|
175
|
+
el.setAttribute('title', binding.value)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### v-permission
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
const vPermission = {
|
|
184
|
+
mounted(el: HTMLElement, binding: DirectiveBinding<string>) {
|
|
185
|
+
if (!hasPermission(binding.value)) {
|
|
186
|
+
el.parentNode?.removeChild(el)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## TypeScript: Global Directives
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
// directives/highlight.ts
|
|
196
|
+
import type { Directive } from 'vue'
|
|
197
|
+
|
|
198
|
+
export type HighlightDirective = Directive<HTMLElement, string>
|
|
199
|
+
|
|
200
|
+
declare module 'vue' {
|
|
201
|
+
export interface ComponentCustomProperties {
|
|
202
|
+
vHighlight: HighlightDirective
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export default {
|
|
207
|
+
mounted: (el, binding) => {
|
|
208
|
+
el.style.backgroundColor = binding.value
|
|
209
|
+
}
|
|
210
|
+
} satisfies HighlightDirective
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Usage on Components
|
|
214
|
+
|
|
215
|
+
⚠️ **Not recommended** - directives apply to root element, which can be unpredictable with multi-root components.
|
|
216
|
+
|
|
217
|
+
```vue-html
|
|
218
|
+
<!-- Applies to MyComponent's root element -->
|
|
219
|
+
<MyComponent v-my-directive />
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
<!--
|
|
223
|
+
Source references:
|
|
224
|
+
- https://vuejs.org/guide/reusability/custom-directives.html
|
|
225
|
+
-->
|
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
# Vue Common Gotchas & Edge Cases
|
|
2
|
+
|
|
3
|
+
Critical Vue 3 gotchas that cause silent failures or hard-to-debug issues.
|
|
4
|
+
|
|
5
|
+
> Based on [vuejs-ai/skills](https://github.com/vuejs-ai/skills) vue-best-practices. For comprehensive coverage (200+ rules), see the upstream repo.
|
|
6
|
+
|
|
7
|
+
## Reactivity
|
|
8
|
+
|
|
9
|
+
### Always Use `.value` When Accessing ref() in Scripts
|
|
10
|
+
|
|
11
|
+
**Impact: HIGH** - Forgetting `.value` causes silent failures.
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
const count = ref(0)
|
|
15
|
+
|
|
16
|
+
// WRONG
|
|
17
|
+
count++ // Tries to increment the ref object
|
|
18
|
+
count = 5 // Reassigns variable, loses reactivity
|
|
19
|
+
items.push(4) // Error: push is not a function
|
|
20
|
+
|
|
21
|
+
// CORRECT
|
|
22
|
+
count.value++
|
|
23
|
+
count.value = 5
|
|
24
|
+
items.value.push(4)
|
|
25
|
+
|
|
26
|
+
// In templates - NO .value needed (Vue unwraps automatically)
|
|
27
|
+
// {{ count }} works, not {{ count.value }}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Never Destructure reactive() Objects Directly
|
|
31
|
+
|
|
32
|
+
**Impact: HIGH** - Destructuring breaks reactive connection.
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
const state = reactive({ count: 0, name: 'Vue' })
|
|
36
|
+
|
|
37
|
+
// WRONG - destructured variables lose reactivity
|
|
38
|
+
const { count, name } = state
|
|
39
|
+
state.count++
|
|
40
|
+
console.log(count) // Still 0!
|
|
41
|
+
|
|
42
|
+
// CORRECT - use toRefs()
|
|
43
|
+
const { count, name } = toRefs(state)
|
|
44
|
+
state.count++
|
|
45
|
+
console.log(count.value) // 1
|
|
46
|
+
|
|
47
|
+
// BEST - just use ref() instead of reactive()
|
|
48
|
+
const count = ref(0)
|
|
49
|
+
const name = ref('Vue')
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Proxy Identity Hazard with reactive()
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
const raw = {}
|
|
56
|
+
const proxy = reactive(raw)
|
|
57
|
+
|
|
58
|
+
// WRONG - comparing different objects
|
|
59
|
+
console.log(proxy === raw) // false
|
|
60
|
+
|
|
61
|
+
// WRONG - creating multiple proxies
|
|
62
|
+
const a = reactive({})
|
|
63
|
+
const b = reactive(a) // Returns same proxy
|
|
64
|
+
console.log(a === b) // true (same object)
|
|
65
|
+
|
|
66
|
+
// GOTCHA - nested objects get proxied too
|
|
67
|
+
const nested = reactive({ obj: {} })
|
|
68
|
+
console.log(nested.obj === nested.obj) // true (same proxy)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Computed Properties
|
|
72
|
+
|
|
73
|
+
### No Side Effects in Computed Getters
|
|
74
|
+
|
|
75
|
+
**Impact: HIGH** - Side effects break reactivity model.
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
// WRONG - mutates state
|
|
79
|
+
const doubled = computed(() => {
|
|
80
|
+
count.value++ // Side effect!
|
|
81
|
+
return count.value * 2
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
// WRONG - async operation
|
|
85
|
+
const data = computed(async () => {
|
|
86
|
+
return await fetch('/api') // Side effect!
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
// CORRECT - pure computation only
|
|
90
|
+
const doubled = computed(() => count.value * 2)
|
|
91
|
+
|
|
92
|
+
// For side effects, use watch:
|
|
93
|
+
watch(count, (newVal) => {
|
|
94
|
+
document.title = `Count: ${newVal}`
|
|
95
|
+
})
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Computed Returns Are Read-Only
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
const fullName = computed(() => `${first.value} ${last.value}`)
|
|
102
|
+
|
|
103
|
+
// WRONG - computed values are read-only
|
|
104
|
+
fullName.value = 'John Doe' // Error!
|
|
105
|
+
|
|
106
|
+
// CORRECT - use writable computed
|
|
107
|
+
const fullName = computed({
|
|
108
|
+
get: () => `${first.value} ${last.value}`,
|
|
109
|
+
set: (val) => {
|
|
110
|
+
const [f, l] = val.split(' ')
|
|
111
|
+
first.value = f
|
|
112
|
+
last.value = l
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Watchers
|
|
118
|
+
|
|
119
|
+
### Clean Up Async Operations to Prevent Race Conditions
|
|
120
|
+
|
|
121
|
+
**Impact: HIGH** - Stale requests can overwrite newer data.
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
const query = ref('')
|
|
125
|
+
const results = ref([])
|
|
126
|
+
|
|
127
|
+
// WRONG - race condition
|
|
128
|
+
watch(query, async (q) => {
|
|
129
|
+
const res = await fetch(`/api?q=${q}`)
|
|
130
|
+
results.value = await res.json() // May overwrite newer results!
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
// CORRECT - use onWatcherCleanup (Vue 3.5+)
|
|
134
|
+
watch(query, async (q) => {
|
|
135
|
+
const controller = new AbortController()
|
|
136
|
+
onWatcherCleanup(() => controller.abort())
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const res = await fetch(`/api?q=${q}`, { signal: controller.signal })
|
|
140
|
+
results.value = await res.json()
|
|
141
|
+
} catch (e) {
|
|
142
|
+
if (e.name !== 'AbortError') throw e
|
|
143
|
+
}
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
// Or use onCleanup parameter
|
|
147
|
+
watch(query, async (q, oldQ, onCleanup) => {
|
|
148
|
+
const controller = new AbortController()
|
|
149
|
+
onCleanup(() => controller.abort())
|
|
150
|
+
// ... same as above
|
|
151
|
+
})
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Deep Watch Returns Same Object Reference
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
const obj = reactive({ nested: { count: 0 } })
|
|
158
|
+
|
|
159
|
+
// GOTCHA - oldValue === newValue for deep watches
|
|
160
|
+
watch(obj, (newVal, oldVal) => {
|
|
161
|
+
console.log(newVal === oldVal) // true! Same object
|
|
162
|
+
}, { deep: true })
|
|
163
|
+
|
|
164
|
+
// If you need old value, clone first:
|
|
165
|
+
watch(
|
|
166
|
+
() => structuredClone(obj),
|
|
167
|
+
(newVal, oldVal) => { /* now different */ }
|
|
168
|
+
)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Props
|
|
172
|
+
|
|
173
|
+
### Props Are Read-Only - Never Mutate
|
|
174
|
+
|
|
175
|
+
**Impact: HIGH** - Breaks one-way data flow.
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
const props = defineProps<{ count: number; user: User }>()
|
|
179
|
+
|
|
180
|
+
// WRONG - direct mutation
|
|
181
|
+
props.count++ // Vue warning
|
|
182
|
+
props.user.name = 'New' // No warning but still wrong!
|
|
183
|
+
|
|
184
|
+
// CORRECT - emit to parent
|
|
185
|
+
const emit = defineEmits(['update:count', 'update-user'])
|
|
186
|
+
emit('update:count', props.count + 1)
|
|
187
|
+
emit('update-user', { ...props.user, name: 'New' })
|
|
188
|
+
|
|
189
|
+
// Or create local copy
|
|
190
|
+
const localUser = ref({ ...props.user })
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Destructured Props Don't Update Watchers (pre-3.5)
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
// WRONG (Vue < 3.5)
|
|
197
|
+
const { count } = defineProps<{ count: number }>()
|
|
198
|
+
watch(count, () => {}) // Won't trigger!
|
|
199
|
+
|
|
200
|
+
// CORRECT - use getter
|
|
201
|
+
const props = defineProps<{ count: number }>()
|
|
202
|
+
watch(() => props.count, () => {})
|
|
203
|
+
|
|
204
|
+
// Vue 3.5+ - destructuring works with reactive props
|
|
205
|
+
const { count } = defineProps<{ count: number }>()
|
|
206
|
+
watch(() => count, () => {}) // Works in 3.5+
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Lifecycle Hooks
|
|
210
|
+
|
|
211
|
+
### Register Hooks Synchronously During Setup
|
|
212
|
+
|
|
213
|
+
**Impact: HIGH** - Async hooks silently fail.
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
// WRONG - hook registered after await
|
|
217
|
+
async setup() {
|
|
218
|
+
const data = await fetchData()
|
|
219
|
+
onMounted(() => {}) // Will NEVER run!
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// WRONG - hook in setTimeout
|
|
223
|
+
setup() {
|
|
224
|
+
setTimeout(() => {
|
|
225
|
+
onMounted(() => {}) // Will NEVER run!
|
|
226
|
+
}, 100)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// CORRECT - register synchronously, async inside
|
|
230
|
+
setup() {
|
|
231
|
+
onMounted(async () => {
|
|
232
|
+
const data = await fetchData()
|
|
233
|
+
})
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
## Templates
|
|
238
|
+
|
|
239
|
+
### Never Use v-if with v-for on Same Element
|
|
240
|
+
|
|
241
|
+
**Impact: HIGH** - Vue 2/3 precedence differs.
|
|
242
|
+
|
|
243
|
+
```vue
|
|
244
|
+
<!-- WRONG - ambiguous precedence -->
|
|
245
|
+
<li v-for="user in users" v-if="user.active" :key="user.id">
|
|
246
|
+
|
|
247
|
+
<!-- Vue 3: v-if runs FIRST, 'user' undefined! -->
|
|
248
|
+
|
|
249
|
+
<!-- CORRECT - computed filter -->
|
|
250
|
+
<li v-for="user in activeUsers" :key="user.id">
|
|
251
|
+
|
|
252
|
+
<script setup>
|
|
253
|
+
const activeUsers = computed(() => users.filter(u => u.active))
|
|
254
|
+
</script>
|
|
255
|
+
|
|
256
|
+
<!-- CORRECT - template wrapper -->
|
|
257
|
+
<template v-for="user in users" :key="user.id">
|
|
258
|
+
<li v-if="user.active">{{ user.name }}</li>
|
|
259
|
+
</template>
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Template Refs Are Null with v-if
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
const inputRef = ref<HTMLInputElement | null>(null)
|
|
266
|
+
|
|
267
|
+
// GOTCHA - ref is null when element hidden
|
|
268
|
+
<input v-if="show" ref="inputRef" />
|
|
269
|
+
|
|
270
|
+
// WRONG - may be null
|
|
271
|
+
inputRef.value.focus() // Error if !show
|
|
272
|
+
|
|
273
|
+
// CORRECT - null check
|
|
274
|
+
inputRef.value?.focus()
|
|
275
|
+
|
|
276
|
+
// Or use watchEffect with flush: 'post'
|
|
277
|
+
watchEffect(() => {
|
|
278
|
+
inputRef.value?.focus()
|
|
279
|
+
}, { flush: 'post' })
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
## defineModel
|
|
283
|
+
|
|
284
|
+
### Object Mutations Don't Emit
|
|
285
|
+
|
|
286
|
+
```ts
|
|
287
|
+
const model = defineModel<{ name: string }>()
|
|
288
|
+
|
|
289
|
+
// WRONG - mutation doesn't notify parent
|
|
290
|
+
model.value.name = 'New' // Parent won't know!
|
|
291
|
+
|
|
292
|
+
// CORRECT - replace entire object
|
|
293
|
+
model.value = { ...model.value, name: 'New' }
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### Updated Value Needs nextTick
|
|
297
|
+
|
|
298
|
+
```ts
|
|
299
|
+
const model = defineModel<string>()
|
|
300
|
+
|
|
301
|
+
// WRONG - value not updated yet
|
|
302
|
+
model.value = 'new'
|
|
303
|
+
console.log(model.value) // Still old value!
|
|
304
|
+
|
|
305
|
+
// CORRECT - wait for nextTick
|
|
306
|
+
model.value = 'new'
|
|
307
|
+
await nextTick()
|
|
308
|
+
console.log(model.value) // Now 'new'
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
## Component Events
|
|
312
|
+
|
|
313
|
+
### Undeclared Emits Can Fire Twice
|
|
314
|
+
|
|
315
|
+
```ts
|
|
316
|
+
// WRONG - missing emit declaration causes double firing
|
|
317
|
+
const emit = defineEmits([]) // 'click' not declared
|
|
318
|
+
<button @click="emit('click')"> // Fires twice!
|
|
319
|
+
|
|
320
|
+
// CORRECT - declare all custom events
|
|
321
|
+
const emit = defineEmits(['click'])
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### Events Don't Bubble Through Components
|
|
325
|
+
|
|
326
|
+
```vue
|
|
327
|
+
<!-- Parent can't listen to grandchild events directly -->
|
|
328
|
+
<Grandparent>
|
|
329
|
+
<Parent>
|
|
330
|
+
<Child @custom="handler" /> <!-- Only Parent can listen -->
|
|
331
|
+
</Parent>
|
|
332
|
+
</Grandparent>
|
|
333
|
+
|
|
334
|
+
<!-- Solution: re-emit or use provide/inject -->
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
## Provide/Inject
|
|
338
|
+
|
|
339
|
+
### Reactivity Not Automatic
|
|
340
|
+
|
|
341
|
+
```ts
|
|
342
|
+
// Provider
|
|
343
|
+
const count = ref(0)
|
|
344
|
+
provide('count', count) // Pass the ref, not .value
|
|
345
|
+
|
|
346
|
+
// Consumer
|
|
347
|
+
const count = inject('count') // Receives the ref
|
|
348
|
+
console.log(count.value) // Reactive!
|
|
349
|
+
|
|
350
|
+
// WRONG - loses reactivity
|
|
351
|
+
provide('count', count.value) // Just passes number
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### Must Call Provide Synchronously
|
|
355
|
+
|
|
356
|
+
```ts
|
|
357
|
+
// WRONG - provide after async
|
|
358
|
+
async setup() {
|
|
359
|
+
await fetchData()
|
|
360
|
+
provide('key', value) // Silently fails!
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// CORRECT
|
|
364
|
+
setup() {
|
|
365
|
+
provide('key', value) // Synchronous
|
|
366
|
+
onMounted(async () => {
|
|
367
|
+
await fetchData()
|
|
368
|
+
})
|
|
369
|
+
}
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
## SSR
|
|
373
|
+
|
|
374
|
+
### Lifecycle Hooks Don't Run on Server
|
|
375
|
+
|
|
376
|
+
```ts
|
|
377
|
+
// onMounted, onUpdated, onUnmounted - client only
|
|
378
|
+
onMounted(() => {
|
|
379
|
+
// Only runs in browser
|
|
380
|
+
window.addEventListener('resize', handler)
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
// For SSR, use onServerPrefetch for data
|
|
384
|
+
onServerPrefetch(async () => {
|
|
385
|
+
data.value = await fetchData()
|
|
386
|
+
})
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
### Hydration Mismatch Causes
|
|
390
|
+
|
|
391
|
+
Common causes:
|
|
392
|
+
|
|
393
|
+
- Browser-only APIs (`window`, `localStorage`)
|
|
394
|
+
- Different timestamps
|
|
395
|
+
- Random values
|
|
396
|
+
- User-agent specific rendering
|
|
397
|
+
|
|
398
|
+
```ts
|
|
399
|
+
// WRONG
|
|
400
|
+
const width = ref(window.innerWidth) // undefined on server
|
|
401
|
+
|
|
402
|
+
// CORRECT
|
|
403
|
+
const width = ref(0)
|
|
404
|
+
onMounted(() => {
|
|
405
|
+
width.value = window.innerWidth
|
|
406
|
+
})
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
## Performance
|
|
410
|
+
|
|
411
|
+
### Use shallowRef for Large Non-Reactive Data
|
|
412
|
+
|
|
413
|
+
```ts
|
|
414
|
+
// WRONG - deep reactivity overhead
|
|
415
|
+
const hugeList = ref(thousandsOfItems)
|
|
416
|
+
|
|
417
|
+
// CORRECT - only track .value assignment
|
|
418
|
+
const hugeList = shallowRef(thousandsOfItems)
|
|
419
|
+
|
|
420
|
+
// Trigger update by replacing entire array
|
|
421
|
+
hugeList.value = [...hugeList.value, newItem]
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
### markRaw for Non-Reactive Objects
|
|
425
|
+
|
|
426
|
+
```ts
|
|
427
|
+
// WRONG - Chart.js instance becomes reactive (breaks it)
|
|
428
|
+
const chart = ref(new Chart(ctx, config))
|
|
429
|
+
|
|
430
|
+
// CORRECT - mark as non-reactive
|
|
431
|
+
const chart = ref(markRaw(new Chart(ctx, config)))
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
## References
|
|
435
|
+
|
|
436
|
+
- [vuejs-ai/skills vue-best-practices](https://github.com/vuejs-ai/skills/tree/main/skills/vue-best-practices) - Full 200+ rules
|
|
437
|
+
- [Vue Style Guide](https://vuejs.org/style-guide/)
|
|
438
|
+
- [Vue 3 Migration Guide](https://v3-migration.vuejs.org/)
|