@codihaus/claude-skills 1.6.18 → 1.6.20
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/knowledge/stacks/_index.md +1 -0
- package/knowledge/stacks/nextjs/_index.md +32 -0
- package/knowledge/stacks/nextjs/references/rsc-patterns.md +705 -0
- package/knowledge/stacks/react/_index.md +21 -0
- package/knowledge/stacks/react/references/performance.md +573 -0
- package/knowledge/stacks/vue/_index.md +750 -0
- package/package.json +1 -1
- package/skills/_registry.md +1 -0
- package/skills/dev-coding/SKILL.md +4 -1
- package/skills/dev-review/SKILL.md +11 -1
|
@@ -0,0 +1,750 @@
|
|
|
1
|
+
# Vue 3
|
|
2
|
+
|
|
3
|
+
> Progressive JavaScript framework for building user interfaces
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
**What it is:**
|
|
8
|
+
- Progressive framework (incrementally adoptable)
|
|
9
|
+
- Component-based with Single File Components (SFCs)
|
|
10
|
+
- Composition API for logic reuse (Vue 3+)
|
|
11
|
+
- Reactive and declarative
|
|
12
|
+
- Virtual DOM with optimized reactivity
|
|
13
|
+
- Template syntax with directives
|
|
14
|
+
|
|
15
|
+
**When to use:**
|
|
16
|
+
- Building interactive user interfaces
|
|
17
|
+
- Single-page applications (SPAs)
|
|
18
|
+
- Progressive enhancement of existing sites
|
|
19
|
+
- Projects needing gentle learning curve
|
|
20
|
+
- Teams wanting flexibility and simplicity
|
|
21
|
+
|
|
22
|
+
**Key concepts:**
|
|
23
|
+
- **SFC (Single File Component)** = .vue files with template, script, style
|
|
24
|
+
- **Composition API** = Composable logic with setup()
|
|
25
|
+
- **Reactivity** = ref(), reactive(), computed(), watch()
|
|
26
|
+
- **Template Syntax** = Directives (v-if, v-for, v-bind, v-on)
|
|
27
|
+
- **Components** = Reusable building blocks
|
|
28
|
+
- **Props & Emits** = Parent-child communication
|
|
29
|
+
- **Provide/Inject** = Dependency injection
|
|
30
|
+
|
|
31
|
+
## Best Practices
|
|
32
|
+
|
|
33
|
+
### Project Structure
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
vue-app/
|
|
37
|
+
├── src/
|
|
38
|
+
│ ├── components/ # Reusable components
|
|
39
|
+
│ │ ├── ui/ # UI primitives
|
|
40
|
+
│ │ ├── forms/ # Form components
|
|
41
|
+
│ │ └── common/ # Shared components
|
|
42
|
+
│ ├── composables/ # Composition API composables
|
|
43
|
+
│ │ └── useAuth.js # use* naming convention
|
|
44
|
+
│ ├── views/ # Page components (routes)
|
|
45
|
+
│ ├── stores/ # Pinia stores
|
|
46
|
+
│ ├── router/ # Vue Router config
|
|
47
|
+
│ ├── assets/ # Static assets
|
|
48
|
+
│ ├── utils/ # Helper functions
|
|
49
|
+
│ ├── types/ # TypeScript types
|
|
50
|
+
│ ├── App.vue # Root component
|
|
51
|
+
│ └── main.js # Entry point
|
|
52
|
+
└── public/ # Public static files
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Component Structure (SFC)
|
|
56
|
+
|
|
57
|
+
```vue
|
|
58
|
+
<!-- ComponentName.vue -->
|
|
59
|
+
<script setup>
|
|
60
|
+
// 1. Imports
|
|
61
|
+
import { ref, computed, watch, onMounted } from 'vue'
|
|
62
|
+
import { useRouter } from 'vue-router'
|
|
63
|
+
|
|
64
|
+
// 2. Props & Emits
|
|
65
|
+
const props = defineProps({
|
|
66
|
+
title: String,
|
|
67
|
+
count: { type: Number, required: true }
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
const emit = defineEmits(['update', 'close'])
|
|
71
|
+
|
|
72
|
+
// 3. Composables
|
|
73
|
+
const router = useRouter()
|
|
74
|
+
const { user } = useAuth()
|
|
75
|
+
|
|
76
|
+
// 4. Reactive state
|
|
77
|
+
const localCount = ref(0)
|
|
78
|
+
const doubled = computed(() => localCount.value * 2)
|
|
79
|
+
|
|
80
|
+
// 5. Methods
|
|
81
|
+
function increment() {
|
|
82
|
+
localCount.value++
|
|
83
|
+
emit('update', localCount.value)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 6. Watchers
|
|
87
|
+
watch(() => props.count, (newVal) => {
|
|
88
|
+
localCount.value = newVal
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
// 7. Lifecycle hooks
|
|
92
|
+
onMounted(() => {
|
|
93
|
+
console.log('Component mounted')
|
|
94
|
+
})
|
|
95
|
+
</script>
|
|
96
|
+
|
|
97
|
+
<template>
|
|
98
|
+
<div class="component">
|
|
99
|
+
<h1>{{ title }}</h1>
|
|
100
|
+
<p>Count: {{ localCount }}</p>
|
|
101
|
+
<p>Doubled: {{ doubled }}</p>
|
|
102
|
+
<button @click="increment">Increment</button>
|
|
103
|
+
</div>
|
|
104
|
+
</template>
|
|
105
|
+
|
|
106
|
+
<style scoped>
|
|
107
|
+
.component {
|
|
108
|
+
/* Component styles */
|
|
109
|
+
}
|
|
110
|
+
</style>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Composition API Best Practices
|
|
114
|
+
|
|
115
|
+
**Using `<script setup>` (Recommended):**
|
|
116
|
+
```vue
|
|
117
|
+
DO:
|
|
118
|
+
✓ Use <script setup> (less boilerplate)
|
|
119
|
+
✓ Name composables with "use" prefix
|
|
120
|
+
✓ Keep composables focused (single concern)
|
|
121
|
+
✓ Return reactive values from composables
|
|
122
|
+
✓ Use ref() for primitives, reactive() for objects
|
|
123
|
+
|
|
124
|
+
DON'T:
|
|
125
|
+
✗ Mix Options API and Composition API
|
|
126
|
+
✗ Use setup() function (use <script setup> instead)
|
|
127
|
+
✗ Destructure reactive objects (loses reactivity)
|
|
128
|
+
✗ Forget .value on refs in script
|
|
129
|
+
✗ Over-abstract into composables too early
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**Reactivity:**
|
|
133
|
+
```javascript
|
|
134
|
+
DO:
|
|
135
|
+
✓ Use ref() for primitives
|
|
136
|
+
const count = ref(0)
|
|
137
|
+
count.value++ // Access with .value in script
|
|
138
|
+
|
|
139
|
+
✓ Use reactive() for objects
|
|
140
|
+
const state = reactive({ count: 0 })
|
|
141
|
+
state.count++ // No .value needed
|
|
142
|
+
|
|
143
|
+
✓ Use computed() for derived state
|
|
144
|
+
const doubled = computed(() => count.value * 2)
|
|
145
|
+
|
|
146
|
+
✓ Use toRefs() when destructuring reactive objects
|
|
147
|
+
const { count } = toRefs(state)
|
|
148
|
+
|
|
149
|
+
DON'T:
|
|
150
|
+
✗ Reassign reactive() (loses reactivity)
|
|
151
|
+
state = { count: 1 } // Wrong
|
|
152
|
+
state.count = 1 // Correct
|
|
153
|
+
|
|
154
|
+
✗ Destructure reactive() directly
|
|
155
|
+
const { count } = reactive({ count: 0 }) // Loses reactivity
|
|
156
|
+
|
|
157
|
+
✗ Use ref() for large objects (use reactive())
|
|
158
|
+
✗ Compute values in watchers (use computed)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
**Composables Pattern:**
|
|
162
|
+
```javascript
|
|
163
|
+
DO:
|
|
164
|
+
✓ Export composable function starting with "use"
|
|
165
|
+
✓ Return reactive values (refs, computed)
|
|
166
|
+
✓ Keep composables composable (can call other composables)
|
|
167
|
+
✓ Handle cleanup in onUnmounted
|
|
168
|
+
|
|
169
|
+
DON'T:
|
|
170
|
+
✗ Call composables conditionally
|
|
171
|
+
✗ Call composables in callbacks
|
|
172
|
+
✗ Call composables outside setup()
|
|
173
|
+
✗ Return plain values (use ref/reactive)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Template Best Practices
|
|
177
|
+
|
|
178
|
+
**Directives:**
|
|
179
|
+
```vue
|
|
180
|
+
DO:
|
|
181
|
+
✓ Use v-bind shorthand (:prop)
|
|
182
|
+
<img :src="imageUrl">
|
|
183
|
+
|
|
184
|
+
✓ Use v-on shorthand (@event)
|
|
185
|
+
<button @click="handler">
|
|
186
|
+
|
|
187
|
+
✓ Use v-for with unique :key
|
|
188
|
+
<div v-for="item in items" :key="item.id">
|
|
189
|
+
|
|
190
|
+
✓ Use v-if for conditional rendering
|
|
191
|
+
<div v-if="show">Content</div>
|
|
192
|
+
|
|
193
|
+
✓ Use v-show for frequent toggles
|
|
194
|
+
<div v-show="visible">Content</div>
|
|
195
|
+
|
|
196
|
+
DON'T:
|
|
197
|
+
✗ Use v-for with v-if on same element (use computed filter)
|
|
198
|
+
<div v-for="item in items" v-if="item.active"> <!-- BAD -->
|
|
199
|
+
|
|
200
|
+
✗ Use index as key for dynamic lists
|
|
201
|
+
v-for="(item, i) in items" :key="i" <!-- BAD -->
|
|
202
|
+
|
|
203
|
+
✗ Forget :key in v-for
|
|
204
|
+
✗ Use v-html with user input (XSS risk)
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
**Component Communication:**
|
|
208
|
+
```vue
|
|
209
|
+
DO:
|
|
210
|
+
✓ Use props for parent → child
|
|
211
|
+
<Child :title="parentTitle" />
|
|
212
|
+
|
|
213
|
+
✓ Use emits for child → parent
|
|
214
|
+
emit('update', value)
|
|
215
|
+
|
|
216
|
+
✓ Use provide/inject for deep hierarchies
|
|
217
|
+
provide('key', value)
|
|
218
|
+
const value = inject('key')
|
|
219
|
+
|
|
220
|
+
✓ Use Pinia for global state
|
|
221
|
+
|
|
222
|
+
DON'T:
|
|
223
|
+
✗ Mutate props directly (one-way data flow)
|
|
224
|
+
props.title = 'new' // Wrong
|
|
225
|
+
emit('update:title', 'new') // Correct
|
|
226
|
+
|
|
227
|
+
✗ Prop drill through many levels (use provide/inject)
|
|
228
|
+
✗ Use event bus (deprecated in Vue 3)
|
|
229
|
+
✗ Access $parent or $root (tight coupling)
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### State Management
|
|
233
|
+
|
|
234
|
+
**Local State:**
|
|
235
|
+
```javascript
|
|
236
|
+
When: Component-specific, not shared
|
|
237
|
+
Example: Form inputs, toggles, local UI state
|
|
238
|
+
Use: ref(), reactive()
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
**Composables:**
|
|
242
|
+
```javascript
|
|
243
|
+
When: Reusable logic, shared between components
|
|
244
|
+
Example: useAuth(), useFetch(), useForm()
|
|
245
|
+
Use: Composable functions returning refs/computed
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
**Pinia (Global Store):**
|
|
249
|
+
```javascript
|
|
250
|
+
When: Global or feature-level state
|
|
251
|
+
Example: Auth state, cart, user preferences
|
|
252
|
+
DO:
|
|
253
|
+
✓ One store per feature/domain
|
|
254
|
+
✓ Use composition stores (setup syntax)
|
|
255
|
+
✓ Keep actions for mutations
|
|
256
|
+
✓ Use getters for computed state
|
|
257
|
+
✓ Modularize stores
|
|
258
|
+
|
|
259
|
+
DON'T:
|
|
260
|
+
✗ Use Vuex (Pinia is official for Vue 3)
|
|
261
|
+
✗ Create one giant store
|
|
262
|
+
✗ Mutate state outside actions
|
|
263
|
+
✗ Store derived data (use getters)
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Performance Optimization
|
|
267
|
+
|
|
268
|
+
**Lazy Loading:**
|
|
269
|
+
```javascript
|
|
270
|
+
DO:
|
|
271
|
+
✓ Lazy load routes
|
|
272
|
+
const Home = () => import('./views/Home.vue')
|
|
273
|
+
|
|
274
|
+
✓ Lazy load heavy components
|
|
275
|
+
const Chart = defineAsyncComponent(() => import('./Chart.vue'))
|
|
276
|
+
|
|
277
|
+
✓ Use Suspense for async components
|
|
278
|
+
<Suspense>
|
|
279
|
+
<AsyncComponent />
|
|
280
|
+
</Suspense>
|
|
281
|
+
|
|
282
|
+
DON'T:
|
|
283
|
+
✗ Lazy load small components (overhead)
|
|
284
|
+
✗ Forget loading/error states
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
**Reactivity Optimization:**
|
|
288
|
+
```javascript
|
|
289
|
+
DO:
|
|
290
|
+
✓ Use shallowRef() for large objects (no deep reactivity)
|
|
291
|
+
✓ Use shallowReactive() for large structures
|
|
292
|
+
✓ Mark constants with markRaw()
|
|
293
|
+
✓ Use v-once for static content
|
|
294
|
+
✓ Use v-memo for expensive list items
|
|
295
|
+
|
|
296
|
+
DON'T:
|
|
297
|
+
✗ Create reactivity for everything
|
|
298
|
+
✗ Deep watch large objects unnecessarily
|
|
299
|
+
✗ Forget to unwatch in cleanup
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
**List Rendering:**
|
|
303
|
+
```vue
|
|
304
|
+
DO:
|
|
305
|
+
✓ Use unique, stable keys (IDs from data)
|
|
306
|
+
<div v-for="item in items" :key="item.id">
|
|
307
|
+
|
|
308
|
+
✓ Filter/sort in computed, not in template
|
|
309
|
+
const filtered = computed(() => items.filter(fn))
|
|
310
|
+
|
|
311
|
+
✓ Use v-memo for expensive items
|
|
312
|
+
<div v-for="item in items" :key="item.id" v-memo="[item.id]">
|
|
313
|
+
|
|
314
|
+
DON'T:
|
|
315
|
+
✗ Use index as key for dynamic lists
|
|
316
|
+
✗ Filter/sort directly in v-for
|
|
317
|
+
✗ Create objects/arrays in template (re-renders)
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Forms & v-model
|
|
321
|
+
|
|
322
|
+
```vue
|
|
323
|
+
DO:
|
|
324
|
+
✓ Use v-model for two-way binding
|
|
325
|
+
<input v-model="text">
|
|
326
|
+
|
|
327
|
+
✓ Use v-model modifiers
|
|
328
|
+
<input v-model.trim="text">
|
|
329
|
+
<input v-model.number="age">
|
|
330
|
+
|
|
331
|
+
✓ Use custom v-model on components
|
|
332
|
+
defineProps(['modelValue'])
|
|
333
|
+
defineEmits(['update:modelValue'])
|
|
334
|
+
|
|
335
|
+
✓ Validate on blur or submit
|
|
336
|
+
|
|
337
|
+
DON'T:
|
|
338
|
+
✗ Validate on every keystroke (performance)
|
|
339
|
+
✗ Forget to trim/sanitize inputs
|
|
340
|
+
✗ Store all form state in parent (use composables)
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### TypeScript Integration
|
|
344
|
+
|
|
345
|
+
```typescript
|
|
346
|
+
DO:
|
|
347
|
+
✓ Define prop types with TypeScript
|
|
348
|
+
defineProps<{
|
|
349
|
+
title: string
|
|
350
|
+
count?: number
|
|
351
|
+
}>()
|
|
352
|
+
|
|
353
|
+
✓ Type emit functions
|
|
354
|
+
const emit = defineEmits<{
|
|
355
|
+
update: [value: number]
|
|
356
|
+
close: []
|
|
357
|
+
}>()
|
|
358
|
+
|
|
359
|
+
✓ Type composables return values
|
|
360
|
+
function useCounter(): { count: Ref<number>, increment: () => void }
|
|
361
|
+
|
|
362
|
+
✓ Use generic components
|
|
363
|
+
defineComponent<Props, Emits>()
|
|
364
|
+
|
|
365
|
+
DON'T:
|
|
366
|
+
✗ Use PropType with <script setup> (use TS interface)
|
|
367
|
+
✗ Forget to type composables
|
|
368
|
+
✗ Over-use any
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
## Anti-Patterns
|
|
372
|
+
|
|
373
|
+
### Common Mistakes
|
|
374
|
+
|
|
375
|
+
```vue
|
|
376
|
+
❌ Mutating props
|
|
377
|
+
props.value = 'new' // Wrong
|
|
378
|
+
emit('update:value', 'new') // Correct
|
|
379
|
+
|
|
380
|
+
❌ v-for without :key
|
|
381
|
+
<div v-for="item in items"> <!-- Missing :key -->
|
|
382
|
+
|
|
383
|
+
❌ v-for + v-if on same element
|
|
384
|
+
<div v-for="item in items" v-if="item.active"> <!-- Use computed -->
|
|
385
|
+
|
|
386
|
+
❌ Destructuring reactive without toRefs
|
|
387
|
+
const { count } = reactive({ count: 0 }) // Loses reactivity
|
|
388
|
+
const { count } = toRefs(reactive({ count: 0 })) // Correct
|
|
389
|
+
|
|
390
|
+
❌ Not accessing .value on refs in script
|
|
391
|
+
const count = ref(0)
|
|
392
|
+
console.log(count) // Wrong: Ref object
|
|
393
|
+
console.log(count.value) // Correct
|
|
394
|
+
|
|
395
|
+
❌ Creating reactive values in template
|
|
396
|
+
<Component :data="{ value: x }" /> <!-- New object every render -->
|
|
397
|
+
|
|
398
|
+
❌ Large computed without caching
|
|
399
|
+
// Computed should be pure and efficient
|
|
400
|
+
const expensive = computed(() => {
|
|
401
|
+
// Heavy calculation on every access
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
❌ Conditional composables
|
|
405
|
+
if (condition) {
|
|
406
|
+
const data = useData() // Wrong: must be at top level
|
|
407
|
+
}
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
## For /dev-specs
|
|
411
|
+
|
|
412
|
+
When writing specs for Vue 3 projects:
|
|
413
|
+
|
|
414
|
+
**Component Specs:**
|
|
415
|
+
- Define props (name, type, required, default)
|
|
416
|
+
- Define emits (event names, payload types)
|
|
417
|
+
- Specify state management approach (local, composable, Pinia)
|
|
418
|
+
- Identify if lazy loading needed
|
|
419
|
+
- Note if TypeScript types required
|
|
420
|
+
|
|
421
|
+
**State Management:**
|
|
422
|
+
- Clarify scope (local, composable, Pinia store)
|
|
423
|
+
- If Pinia: specify store name and location
|
|
424
|
+
- If composable: specify composable name and return shape
|
|
425
|
+
|
|
426
|
+
**Performance Requirements:**
|
|
427
|
+
- List optimization needs (lazy load, v-memo, shallow)
|
|
428
|
+
- Specify bundle size constraints
|
|
429
|
+
|
|
430
|
+
**Patterns to Use:**
|
|
431
|
+
- Composables for reusable logic
|
|
432
|
+
- Provide/inject for deep prop drilling
|
|
433
|
+
- Pinia stores for global state
|
|
434
|
+
|
|
435
|
+
## For /dev-coding
|
|
436
|
+
|
|
437
|
+
When implementing Vue 3 features:
|
|
438
|
+
|
|
439
|
+
### 1. Component Creation
|
|
440
|
+
|
|
441
|
+
**Start with:**
|
|
442
|
+
- Read tech-context.md for project's Vue patterns
|
|
443
|
+
- Check if TypeScript is used
|
|
444
|
+
- Identify state management approach (Pinia, composables)
|
|
445
|
+
- Check for UI library (Vuetify, PrimeVue, Element Plus, etc.)
|
|
446
|
+
|
|
447
|
+
**Structure:**
|
|
448
|
+
```vue
|
|
449
|
+
<script setup lang="ts">
|
|
450
|
+
// 1. Imports
|
|
451
|
+
import { ref, computed, watch, onMounted } from 'vue'
|
|
452
|
+
|
|
453
|
+
// 2. Props & Emits
|
|
454
|
+
const props = defineProps<{
|
|
455
|
+
title: string
|
|
456
|
+
count: number
|
|
457
|
+
}>()
|
|
458
|
+
|
|
459
|
+
const emit = defineEmits<{
|
|
460
|
+
update: [value: number]
|
|
461
|
+
}>()
|
|
462
|
+
|
|
463
|
+
// 3. Composables
|
|
464
|
+
const { user, loading } = useAuth()
|
|
465
|
+
|
|
466
|
+
// 4. State
|
|
467
|
+
const localState = ref(0)
|
|
468
|
+
const derived = computed(() => localState.value * 2)
|
|
469
|
+
|
|
470
|
+
// 5. Methods
|
|
471
|
+
function handleClick() {
|
|
472
|
+
emit('update', localState.value)
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// 6. Watchers
|
|
476
|
+
watch(() => props.count, (newVal) => {
|
|
477
|
+
localState.value = newVal
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
// 7. Lifecycle
|
|
481
|
+
onMounted(() => {
|
|
482
|
+
// Setup logic
|
|
483
|
+
})
|
|
484
|
+
</script>
|
|
485
|
+
|
|
486
|
+
<template>
|
|
487
|
+
<div>
|
|
488
|
+
<!-- Template -->
|
|
489
|
+
</div>
|
|
490
|
+
</template>
|
|
491
|
+
|
|
492
|
+
<style scoped>
|
|
493
|
+
/* Scoped styles */
|
|
494
|
+
</style>
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
### 2. State Management
|
|
498
|
+
|
|
499
|
+
**Choose based on scope:**
|
|
500
|
+
- **Local UI state** → ref(), reactive()
|
|
501
|
+
- **Reusable logic** → Composable (useX)
|
|
502
|
+
- **Feature-level** → Pinia store
|
|
503
|
+
- **Deep hierarchy** → provide/inject
|
|
504
|
+
- **Global config** → app.config.globalProperties
|
|
505
|
+
|
|
506
|
+
### 3. Composables
|
|
507
|
+
|
|
508
|
+
**Create reusable logic:**
|
|
509
|
+
```javascript
|
|
510
|
+
// composables/useCounter.js
|
|
511
|
+
import { ref, computed } from 'vue'
|
|
512
|
+
|
|
513
|
+
export function useCounter(initial = 0) {
|
|
514
|
+
const count = ref(initial)
|
|
515
|
+
const doubled = computed(() => count.value * 2)
|
|
516
|
+
|
|
517
|
+
function increment() {
|
|
518
|
+
count.value++
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return { count, doubled, increment }
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Usage in component
|
|
525
|
+
const { count, doubled, increment } = useCounter(10)
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
### 4. Pinia Store
|
|
529
|
+
|
|
530
|
+
**Create feature store:**
|
|
531
|
+
```javascript
|
|
532
|
+
// stores/auth.js
|
|
533
|
+
import { defineStore } from 'pinia'
|
|
534
|
+
import { ref, computed } from 'vue'
|
|
535
|
+
|
|
536
|
+
export const useAuthStore = defineStore('auth', () => {
|
|
537
|
+
// State
|
|
538
|
+
const user = ref(null)
|
|
539
|
+
const token = ref(null)
|
|
540
|
+
|
|
541
|
+
// Getters
|
|
542
|
+
const isAuthenticated = computed(() => !!token.value)
|
|
543
|
+
|
|
544
|
+
// Actions
|
|
545
|
+
async function login(credentials) {
|
|
546
|
+
const response = await api.login(credentials)
|
|
547
|
+
user.value = response.user
|
|
548
|
+
token.value = response.token
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function logout() {
|
|
552
|
+
user.value = null
|
|
553
|
+
token.value = null
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
return { user, token, isAuthenticated, login, logout }
|
|
557
|
+
})
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
### 5. Common Patterns
|
|
561
|
+
|
|
562
|
+
**Fetch data:**
|
|
563
|
+
```vue
|
|
564
|
+
<script setup>
|
|
565
|
+
import { ref, onMounted } from 'vue'
|
|
566
|
+
|
|
567
|
+
const data = ref(null)
|
|
568
|
+
const loading = ref(true)
|
|
569
|
+
const error = ref(null)
|
|
570
|
+
|
|
571
|
+
onMounted(async () => {
|
|
572
|
+
try {
|
|
573
|
+
data.value = await api.getData()
|
|
574
|
+
} catch (err) {
|
|
575
|
+
error.value = err
|
|
576
|
+
} finally {
|
|
577
|
+
loading.value = false
|
|
578
|
+
}
|
|
579
|
+
})
|
|
580
|
+
</script>
|
|
581
|
+
|
|
582
|
+
<template>
|
|
583
|
+
<div v-if="loading">Loading...</div>
|
|
584
|
+
<div v-else-if="error">Error: {{ error.message }}</div>
|
|
585
|
+
<div v-else>{{ data }}</div>
|
|
586
|
+
</template>
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
**Form handling:**
|
|
590
|
+
```vue
|
|
591
|
+
<script setup>
|
|
592
|
+
import { reactive, computed } from 'vue'
|
|
593
|
+
|
|
594
|
+
const form = reactive({
|
|
595
|
+
email: '',
|
|
596
|
+
password: ''
|
|
597
|
+
})
|
|
598
|
+
|
|
599
|
+
const errors = reactive({
|
|
600
|
+
email: '',
|
|
601
|
+
password: ''
|
|
602
|
+
})
|
|
603
|
+
|
|
604
|
+
const isValid = computed(() =>
|
|
605
|
+
form.email && form.password && !errors.email && !errors.password
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
function validateEmail() {
|
|
609
|
+
errors.email = form.email.includes('@') ? '' : 'Invalid email'
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
async function submit() {
|
|
613
|
+
if (!isValid.value) return
|
|
614
|
+
await api.login(form)
|
|
615
|
+
}
|
|
616
|
+
</script>
|
|
617
|
+
|
|
618
|
+
<template>
|
|
619
|
+
<form @submit.prevent="submit">
|
|
620
|
+
<input
|
|
621
|
+
v-model="form.email"
|
|
622
|
+
@blur="validateEmail"
|
|
623
|
+
:class="{ error: errors.email }"
|
|
624
|
+
>
|
|
625
|
+
<span v-if="errors.email">{{ errors.email }}</span>
|
|
626
|
+
|
|
627
|
+
<input v-model="form.password" type="password">
|
|
628
|
+
|
|
629
|
+
<button :disabled="!isValid">Login</button>
|
|
630
|
+
</form>
|
|
631
|
+
</template>
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
## For /dev-review
|
|
635
|
+
|
|
636
|
+
When reviewing Vue 3 code:
|
|
637
|
+
|
|
638
|
+
**Component Quality:**
|
|
639
|
+
- [ ] Uses `<script setup>` (not Options API)
|
|
640
|
+
- [ ] Props properly typed (TypeScript or PropType)
|
|
641
|
+
- [ ] Emits properly defined
|
|
642
|
+
- [ ] Component names are PascalCase
|
|
643
|
+
- [ ] SFC order: script → template → style
|
|
644
|
+
|
|
645
|
+
**Composition API:**
|
|
646
|
+
- [ ] Composables follow "use*" naming
|
|
647
|
+
- [ ] No conditional composable calls
|
|
648
|
+
- [ ] Composables called at top level of setup
|
|
649
|
+
- [ ] Reactive values returned from composables
|
|
650
|
+
- [ ] Cleanup handled (watchers, listeners)
|
|
651
|
+
|
|
652
|
+
**Reactivity:**
|
|
653
|
+
- [ ] ref() used for primitives
|
|
654
|
+
- [ ] reactive() used for objects
|
|
655
|
+
- [ ] .value accessed in script (not template)
|
|
656
|
+
- [ ] No destructuring reactive without toRefs
|
|
657
|
+
- [ ] No reassigning reactive objects
|
|
658
|
+
- [ ] computed() for derived state
|
|
659
|
+
- [ ] No complex logic in template
|
|
660
|
+
|
|
661
|
+
**Template:**
|
|
662
|
+
- [ ] v-for has unique :key
|
|
663
|
+
- [ ] No v-for + v-if on same element
|
|
664
|
+
- [ ] v-bind/v-on use shorthands (: and @)
|
|
665
|
+
- [ ] No inline objects/arrays (causes re-renders)
|
|
666
|
+
- [ ] Proper event naming (kebab-case)
|
|
667
|
+
|
|
668
|
+
**State Management:**
|
|
669
|
+
- [ ] Appropriate scope (local, composable, store)
|
|
670
|
+
- [ ] Props not mutated directly
|
|
671
|
+
- [ ] Emits used for child → parent
|
|
672
|
+
- [ ] Pinia stores follow composition pattern
|
|
673
|
+
- [ ] No Vuex (use Pinia)
|
|
674
|
+
|
|
675
|
+
**Performance:**
|
|
676
|
+
- [ ] Heavy components lazy loaded
|
|
677
|
+
- [ ] Large lists use stable keys
|
|
678
|
+
- [ ] v-memo used for expensive items
|
|
679
|
+
- [ ] No unnecessary reactivity (shallowRef/shallowReactive)
|
|
680
|
+
- [ ] Watchers cleaned up
|
|
681
|
+
|
|
682
|
+
**TypeScript:**
|
|
683
|
+
- [ ] Props typed properly
|
|
684
|
+
- [ ] Emits typed
|
|
685
|
+
- [ ] Composables return types defined
|
|
686
|
+
- [ ] No excessive use of any
|
|
687
|
+
|
|
688
|
+
## Integration with Other Stacks
|
|
689
|
+
|
|
690
|
+
**Vue + Nuxt:**
|
|
691
|
+
- Follow Nuxt conventions (pages/, composables/, server/)
|
|
692
|
+
- Use auto-imports
|
|
693
|
+
- Use Nuxt composables (useAsyncData, useFetch, etc.)
|
|
694
|
+
- Follow SSR guidelines
|
|
695
|
+
|
|
696
|
+
**Vue + Vite:**
|
|
697
|
+
- Fast dev server with HMR
|
|
698
|
+
- Use Vite plugins
|
|
699
|
+
- Leverage Vite's optimized build
|
|
700
|
+
|
|
701
|
+
**Vue + TypeScript:**
|
|
702
|
+
- Use `<script setup lang="ts">`
|
|
703
|
+
- Define props with interface
|
|
704
|
+
- Type emits and composables
|
|
705
|
+
- Use generic components
|
|
706
|
+
|
|
707
|
+
**Vue + Pinia:**
|
|
708
|
+
- Composition-style stores
|
|
709
|
+
- TypeScript support built-in
|
|
710
|
+
- DevTools integration
|
|
711
|
+
- Hot module replacement
|
|
712
|
+
|
|
713
|
+
## Common Gotchas
|
|
714
|
+
|
|
715
|
+
1. **Ref .value**: Forgetting .value in script, adding .value in template
|
|
716
|
+
2. **Destructuring Reactivity**: Losing reactivity when destructuring reactive()
|
|
717
|
+
3. **Conditional Composables**: Calling composables inside if/loops
|
|
718
|
+
4. **Props Mutation**: Mutating props directly instead of emitting
|
|
719
|
+
5. **v-for Key**: Using index or missing key entirely
|
|
720
|
+
6. **v-for + v-if**: Combining on same element (use computed filter)
|
|
721
|
+
7. **Template Refs**: Accessing $refs before onMounted
|
|
722
|
+
8. **Async Setup**: Can't use async setup() without Suspense
|
|
723
|
+
|
|
724
|
+
## Vue 3 Features
|
|
725
|
+
|
|
726
|
+
**Composition API:**
|
|
727
|
+
- setup() and <script setup>
|
|
728
|
+
- Composables for logic reuse
|
|
729
|
+
- Better TypeScript support
|
|
730
|
+
- More flexible code organization
|
|
731
|
+
|
|
732
|
+
**New APIs:**
|
|
733
|
+
- Teleport (render outside component hierarchy)
|
|
734
|
+
- Fragments (multiple root nodes)
|
|
735
|
+
- Suspense (async component loading)
|
|
736
|
+
- defineAsyncComponent (lazy loading)
|
|
737
|
+
|
|
738
|
+
**Performance:**
|
|
739
|
+
- Faster reactivity system (Proxy-based)
|
|
740
|
+
- Smaller bundle size
|
|
741
|
+
- Better tree-shaking
|
|
742
|
+
- Optimized virtual DOM
|
|
743
|
+
|
|
744
|
+
## Resources
|
|
745
|
+
|
|
746
|
+
- [Vue 3 Docs](https://vuejs.org) - Official documentation
|
|
747
|
+
- [Vue DevTools](https://devtools.vuejs.org) - Browser extension
|
|
748
|
+
- [Pinia](https://pinia.vuejs.org) - Official state management
|
|
749
|
+
- [Vue Router](https://router.vuejs.org) - Official router
|
|
750
|
+
- [Vite](https://vitejs.dev) - Recommended build tool
|