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.
Files changed (66) hide show
  1. package/.claude/.install.log +4 -0
  2. package/.claude/settings.json +64 -0
  3. package/.claude/settings.local.json +7 -0
  4. package/.claude/skills/agentation/.meta.json +6 -0
  5. package/.claude/skills/agentation/SKILL.md +107 -0
  6. package/.claude/skills/fe-biz-patterns/.meta.json +6 -0
  7. package/.claude/skills/fe-biz-patterns/SKILL.md +26 -0
  8. package/.claude/skills/fe-biz-patterns/references/infinite-scroll.md +292 -0
  9. package/.claude/skills/fe-biz-patterns/references/pinia-store.md +174 -0
  10. package/.claude/skills/fe-biz-patterns/references/service-layer.md +198 -0
  11. package/.claude/skills/fe-biz-patterns/references/tab-anchor.md +1125 -0
  12. package/.claude/skills/fe-biz-patterns/references/use-loading.md +114 -0
  13. package/.claude/skills/frontend-code-review/.meta.json +6 -0
  14. package/.claude/skills/frontend-code-review/SKILL.md +167 -0
  15. package/.claude/skills/frontend-code-review/references/checklist.md +298 -0
  16. package/.claude/skills/frontend-design/.meta.json +6 -0
  17. package/.claude/skills/frontend-design/LICENSE.txt +177 -0
  18. package/.claude/skills/frontend-design/SKILL.md +42 -0
  19. package/.claude/skills/moai-framework-electron/.meta.json +6 -0
  20. package/.claude/skills/moai-framework-electron/SKILL.md +328 -0
  21. package/.claude/skills/skill-creator/.meta.json +6 -0
  22. package/.claude/skills/skill-creator/SKILL.md +356 -0
  23. package/.claude/skills/skill-creator/references/output-patterns.md +82 -0
  24. package/.claude/skills/skill-creator/references/workflows.md +28 -0
  25. package/.claude/skills/skill-creator/scripts/init_skill.py +303 -0
  26. package/.claude/skills/skill-creator/scripts/package_skill.py +110 -0
  27. package/.claude/skills/skill-creator/scripts/quick_validate.py +95 -0
  28. package/.claude/skills/ui-ux-pro-max/.meta.json +6 -0
  29. package/.claude/skills/ui-ux-pro-max/SKILL.md +228 -0
  30. package/.claude/skills/ui-ux-pro-max/data/charts.csv +26 -0
  31. package/.claude/skills/ui-ux-pro-max/data/colors.csv +97 -0
  32. package/.claude/skills/ui-ux-pro-max/data/landing.csv +31 -0
  33. package/.claude/skills/ui-ux-pro-max/data/products.csv +97 -0
  34. package/.claude/skills/ui-ux-pro-max/data/prompts.csv +24 -0
  35. package/.claude/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
  36. package/.claude/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
  37. package/.claude/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
  38. package/.claude/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
  39. package/.claude/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
  40. package/.claude/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
  41. package/.claude/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
  42. package/.claude/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
  43. package/.claude/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
  44. package/.claude/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
  45. package/.claude/skills/ui-ux-pro-max/data/styles.csv +59 -0
  46. package/.claude/skills/ui-ux-pro-max/data/typography.csv +58 -0
  47. package/.claude/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
  48. package/.claude/skills/ui-ux-pro-max/scripts/core.py +238 -0
  49. package/.claude/skills/ui-ux-pro-max/scripts/search.py +61 -0
  50. package/.claude/skills/vue/.meta.json +6 -0
  51. package/.claude/skills/vue/SKILL.md +103 -0
  52. package/.claude/skills/vue/references/components.md +323 -0
  53. package/.claude/skills/vue/references/composables.md +358 -0
  54. package/.claude/skills/vue/references/directives.md +225 -0
  55. package/.claude/skills/vue/references/gotchas.md +438 -0
  56. package/.claude/skills/vue/references/provide-inject.md +174 -0
  57. package/.claude/skills/vue/references/reactivity.md +289 -0
  58. package/.claude/skills/vue/references/router.md +181 -0
  59. package/.claude/skills/vue/references/testing.md +294 -0
  60. package/.claude/skills/vue/references/typescript.md +172 -0
  61. package/.claude/skills/vue/references/utils-client.md +156 -0
  62. package/CLAUDE.md +131 -0
  63. package/package.json +23 -0
  64. package/scripts/cli.js +260 -0
  65. package/scripts/copy-skills.js +86 -0
  66. package/scripts/core.js +256 -0
@@ -0,0 +1,323 @@
1
+ # Vue Components
2
+
3
+ Patterns for Vue 3 components using Composition API with `<script setup>`.
4
+
5
+ ## Quick Reference
6
+
7
+ | Pattern | Syntax |
8
+ | --------------------- | --------------------------------------------------------------- |
9
+ | Props (destructured) | `const { name = 'default' } = defineProps<{ name?: string }>()` |
10
+ | Props (template-only) | `defineProps<{ name: string }>()` |
11
+ | Emits | `const emit = defineEmits<{ click: [id: number] }>()` |
12
+ | Two-way binding | `const model = defineModel<string>()` |
13
+ | Slots shorthand | `<template #header>` not `<template v-slot:header>` |
14
+
15
+ ## Naming
16
+
17
+ **Files:** PascalCase (`UserProfile.vue`) OR kebab-case (`user-profile.vue`) - be consistent
18
+
19
+ **Component names in code:** Always PascalCase
20
+
21
+ **Composition:** General → Specific: `SearchButtonClear.vue` not `ClearSearchButton.vue`
22
+
23
+ ## Props
24
+
25
+ **Destructure with defaults (Vue 3.5+)** when used in script or need defaults:
26
+
27
+ ```ts
28
+ const { count = 0, message = 'Hello' } = defineProps<{
29
+ count?: number
30
+ message?: string
31
+ required: boolean
32
+ }>()
33
+
34
+ // Use directly - maintains reactivity
35
+ console.log(count + 1)
36
+
37
+ // ⚠️ When passing to watchers/functions, wrap in getter:
38
+ watch(() => count, (newVal) => { ... }) // ✅ Correct
39
+ watch(count, (newVal) => { ... }) // ❌ Won't work
40
+ ```
41
+
42
+ **Non-destructured** only if props ONLY used in template:
43
+
44
+ ```ts
45
+ defineProps<{ count: number }>()
46
+ // Template: {{ count }}
47
+ ```
48
+
49
+ **Same-name shorthand (Vue 3.4+):** `:count` instead of `:count="count"`
50
+
51
+ ```vue
52
+ <MyComponent :count :user :items />
53
+ <!-- Same as: :count="count" :user="user" :items="items" -->
54
+ ```
55
+
56
+ [Reactive destructuring docs](https://vuejs.org/guide/components/props#reactive-props-destructure)
57
+
58
+ ## Emits
59
+
60
+ Type-safe event definitions:
61
+
62
+ ```ts
63
+ const emit = defineEmits<{
64
+ update: [id: number, value: string] // multiple args
65
+ close: [] // no args
66
+ }>()
67
+
68
+ // Usage
69
+ emit('update', 123, 'new value')
70
+ emit('close')
71
+ ```
72
+
73
+ **Template syntax:** kebab-case (`@update-item`) vs camelCase in script (`updateItem`)
74
+
75
+ ## Slots
76
+
77
+ **Always use shorthand:** `<template #header>` not `<template v-slot:header>`
78
+
79
+ **Always explicit `<template>` tags** for all slots
80
+
81
+ ```vue
82
+ <template>
83
+ <Card>
84
+ <template #header>
85
+ <h2>Title</h2>
86
+ </template>
87
+ <template #default>
88
+ Content
89
+ </template>
90
+ </Card>
91
+ </template>
92
+ ```
93
+
94
+ ## defineModel() - Two-Way Binding
95
+
96
+ Replaces manual `modelValue` prop + `update:modelValue` emit.
97
+
98
+ ### Basic
99
+
100
+ ```vue
101
+ <script setup lang="ts">
102
+ const title = defineModel<string>()
103
+ </script>
104
+
105
+ <template>
106
+ <input v-model="title">
107
+ </template>
108
+ ```
109
+
110
+ ### With Options
111
+
112
+ ```vue
113
+ <script setup lang="ts">
114
+ const [title, modifiers] = defineModel<string>({
115
+ default: 'default value',
116
+ required: true,
117
+ get: (value) => value.trim(),
118
+ set: (value) => {
119
+ if (modifiers.capitalize) {
120
+ return value.charAt(0).toUpperCase() + value.slice(1)
121
+ }
122
+ return value
123
+ },
124
+ })
125
+ </script>
126
+ ```
127
+
128
+ **⚠️ Warning:** When using `default` without parent providing a value, parent and child can de-sync (parent `undefined`, child has default). Always provide matching defaults in parent or make prop required.
129
+
130
+ **Prevent double-emit with `required: true`:**
131
+
132
+ ```ts
133
+ // ❌ Without required - emits twice (undefined then value)
134
+ const model = defineModel<Item>()
135
+
136
+ // ✅ With required - single emit
137
+ const model = defineModel<Item>({ required: true })
138
+ ```
139
+
140
+ Use `required: true` when the model should always have a value to avoid the double-emit issue during initialization.
141
+
142
+ ### Multiple Models
143
+
144
+ Default assumes `modelValue` prop. For multiple bindings, use explicit names:
145
+
146
+ ```vue
147
+ <script setup lang="ts">
148
+ const firstName = defineModel<string>('firstName')
149
+ const age = defineModel<number>('age')
150
+ </script>
151
+
152
+ <!-- Usage -->
153
+ <UserForm v-model:first-name="user.firstName" v-model:age="user.age" />
154
+ ```
155
+
156
+ [v-model modifiers docs](https://vuejs.org/guide/components/v-model#handling-v-model-modifiers)
157
+
158
+ ## Reusable Templates
159
+
160
+ For typed, scoped template snippets within a component:
161
+
162
+ ```vue
163
+ <script setup lang="ts">
164
+ import { createReusableTemplate } from '@vueuse/core'
165
+
166
+ const [DefineItem, UseItem] = createReusableTemplate<{
167
+ item: SearchItem
168
+ icon: string
169
+ color?: 'red' | 'green' | 'blue'
170
+ }>()
171
+ </script>
172
+
173
+ <template>
174
+ <DefineItem v-slot="{ item, icon, color }">
175
+ <div :class="color">
176
+ <Icon :name="icon" />
177
+ {{ item.name }}
178
+ </div>
179
+ </DefineItem>
180
+
181
+ <!-- Reuse multiple times -->
182
+ <UseItem v-for="item in items" :key="item.id" :item :icon="getIcon(item)" />
183
+ </template>
184
+ ```
185
+
186
+ ## Template Refs (Vue 3.5+)
187
+
188
+ Use `useTemplateRef()` for type-safe template references with IDE support:
189
+
190
+ ```vue
191
+ <script setup lang="ts">
192
+ import { useTemplateRef, onMounted } from 'vue'
193
+
194
+ const input = useTemplateRef<HTMLInputElement>('my-input')
195
+
196
+ onMounted(() => {
197
+ input.value?.focus()
198
+ })
199
+ </script>
200
+
201
+ <template>
202
+ <input ref="my-input">
203
+ </template>
204
+ ```
205
+
206
+ **Benefits over `ref()`:**
207
+
208
+ - Type-safe with generics
209
+ - Better IDE autocomplete and refactoring
210
+ - Explicit ref name as string literal
211
+
212
+ **Dynamic refs:**
213
+
214
+ ```vue
215
+ <script setup lang="ts">
216
+ const items = ref(['a', 'b', 'c'])
217
+ const itemRefs = useTemplateRef<HTMLElement>('item')
218
+
219
+ // Access refs after mount
220
+ onMounted(() => {
221
+ console.log(itemRefs.value) // Array of elements
222
+ })
223
+ </script>
224
+
225
+ <template>
226
+ <div v-for="item in items" :key="item" ref="item">
227
+ {{ item }}
228
+ </div>
229
+ </template>
230
+ ```
231
+
232
+ **Component refs with generics:**
233
+
234
+ For generic components, use `ComponentExposed` from `vue-component-type-helpers`:
235
+
236
+ ```ts
237
+ import type { ComponentExposed } from 'vue-component-type-helpers'
238
+ import MyGenericComponent from './MyGenericComponent.vue'
239
+
240
+ // Get exposed methods/properties with correct generic types
241
+ const compRef = useTemplateRef<ComponentExposed<typeof MyGenericComponent>>('comp')
242
+
243
+ onMounted(() => {
244
+ compRef.value?.someExposedMethod() // Typed!
245
+ })
246
+ ```
247
+
248
+ Install: `pnpm add -D vue-component-type-helpers`
249
+
250
+ ## SSR Hydration (Vue 3.5+)
251
+
252
+ **Suppress hydration mismatches** for values that differ between server/client:
253
+
254
+ ```vue
255
+ <template>
256
+ <!-- Client-side only values -->
257
+ <span data-allow-mismatch>{{ new Date().toLocaleString() }}</span>
258
+
259
+ <!-- Specific mismatch types -->
260
+ <span data-allow-mismatch="text">{{ timestamp }}</span>
261
+ <span data-allow-mismatch="children">
262
+ <ClientOnly>...</ClientOnly>
263
+ </span>
264
+ <span data-allow-mismatch="style">...</span>
265
+ <span data-allow-mismatch="class">...</span>
266
+ <span data-allow-mismatch="attribute">...</span>
267
+ </template>
268
+ ```
269
+
270
+ **Generate SSR-stable IDs:**
271
+
272
+ ```vue
273
+ <script setup lang="ts">
274
+ import { useId } from 'vue'
275
+
276
+ const id = useId() // Stable across server/client renders
277
+ </script>
278
+
279
+ <template>
280
+ <label :for="id">Name</label>
281
+ <input :id="id">
282
+ </template>
283
+ ```
284
+
285
+ ## Deferred Teleport (Vue 3.5+)
286
+
287
+ Teleport to elements rendered later in the same cycle:
288
+
289
+ ```vue
290
+ <template>
291
+ <!-- This renders first -->
292
+ <Teleport defer to="#late-div">
293
+ <span>Deferred content</span>
294
+ </Teleport>
295
+
296
+ <!-- This renders after, but Teleport waits -->
297
+ <div id="late-div"></div>
298
+ </template>
299
+ ```
300
+
301
+ Without `defer`, teleport to `#late-div` would fail since it doesn't exist yet.
302
+
303
+ ## Common Mistakes
304
+
305
+ **Using `const props =` with destructured values:**
306
+
307
+ ```ts
308
+ // ❌ Wrong
309
+ const props = defineProps<{ count: number }>()
310
+ const { count } = props // Loses reactivity
311
+ ```
312
+
313
+ **Forgetting TypeScript types:**
314
+
315
+ ```ts
316
+ // ❌ Wrong
317
+ const emit = defineEmits(['update'])
318
+
319
+ // ✅ Correct
320
+ const emit = defineEmits<{ update: [id: number] }>()
321
+ ```
322
+
323
+ **Components >300 lines:** Split into smaller components or extract logic to composables
@@ -0,0 +1,358 @@
1
+ # Vue Composables
2
+
3
+ Reusable functions encapsulating stateful logic using Composition API.
4
+
5
+ ## Core Rules
6
+
7
+ 1. **VueUse first** - check [vueuse.org](https://vueuse.org) before writing custom
8
+ 2. **No async composables** - lose lifecycle context when awaited in other composables
9
+ 3. **Top-level only** - never call in event handlers, conditionals, or loops
10
+ 4. **readonly() exports** - protect internal state from external mutation
11
+ 5. **useState() for SSR** - use Nuxt's `useState()` not global refs
12
+
13
+ ## Quick Reference
14
+
15
+ | Pattern | Example |
16
+ | --------- | ------------------------------------------------ |
17
+ | Naming | `useAuth`, `useCounter`, `useDebounce` |
18
+ | State | `const count = ref(0)` |
19
+ | Computed | `const double = computed(() => count.value * 2)` |
20
+ | Lifecycle | `onMounted(() => ...)`, `onUnmounted(() => ...)` |
21
+ | Return | `return { count, increment }` |
22
+
23
+ ## Structure
24
+
25
+ ```ts
26
+ // composables/useCounter.ts
27
+ import { readonly, ref } from 'vue'
28
+
29
+ export function useCounter(initialValue = 0) {
30
+ const count = ref(initialValue)
31
+
32
+ function increment() { count.value++ }
33
+ function decrement() { count.value-- }
34
+ function reset() { count.value = initialValue }
35
+
36
+ return {
37
+ count: readonly(count), // readonly if shouldn't be mutated
38
+ increment,
39
+ decrement,
40
+ reset,
41
+ }
42
+ }
43
+ ```
44
+
45
+ ## Naming
46
+
47
+ **Always prefix with `use`:** `useAuth`, `useLocalStorage`, `useDebounce`
48
+
49
+ **File = function:** `useAuth.ts` exports `useAuth`
50
+
51
+ ## Best Practices
52
+
53
+ **Do:**
54
+
55
+ - Return object with named properties (destructuring-friendly)
56
+ - Accept options object for configuration
57
+ - Use `readonly()` for state that shouldn't mutate
58
+ - Handle cleanup (`onUnmounted`, `onScopeDispose`)
59
+ - Add JSDoc for complex functions
60
+
61
+ ## Lifecycle
62
+
63
+ Hooks execute in component context:
64
+
65
+ ```ts
66
+ export function useEventListener(target: EventTarget, event: string, handler: Function) {
67
+ onMounted(() => target.addEventListener(event, handler))
68
+ onUnmounted(() => target.removeEventListener(event, handler))
69
+ }
70
+ ```
71
+
72
+ **Watcher cleanup (Vue 3.5+):**
73
+
74
+ ```ts
75
+ import { watch, onWatcherCleanup } from 'vue'
76
+
77
+ export function usePolling(url: Ref<string>) {
78
+ watch(url, (newUrl) => {
79
+ const interval = setInterval(() => {
80
+ fetch(newUrl).then(/* ... */)
81
+ }, 1000)
82
+
83
+ // Cleanup when watcher re-runs or stops
84
+ onWatcherCleanup(() => {
85
+ clearInterval(interval)
86
+ })
87
+ })
88
+ }
89
+ ```
90
+
91
+ **Benefits of `onWatcherCleanup()`:**
92
+
93
+ - Cleaner than returning cleanup functions
94
+ - Works with async watchers
95
+ - Can be called multiple times in same watcher
96
+
97
+ ## Async Pattern
98
+
99
+ ```ts
100
+ export function useAsyncData<T>(fetcher: () => Promise<T>) {
101
+ const data = ref<T | null>(null)
102
+ const error = ref<Error | null>(null)
103
+ const loading = ref(false)
104
+
105
+ async function execute() {
106
+ loading.value = true
107
+ error.value = null
108
+ try {
109
+ data.value = await fetcher()
110
+ }
111
+ catch (e) {
112
+ error.value = e as Error
113
+ }
114
+ finally {
115
+ loading.value = false
116
+ }
117
+ }
118
+
119
+ execute()
120
+ return { data, error, loading, refetch: execute }
121
+ }
122
+ ```
123
+
124
+ **Data fetching:** Prefer Pinia Colada queries over custom composables.
125
+
126
+ ## VueUse
127
+
128
+ > For VueUse composable reference, use the `vueuse` skill.
129
+
130
+ Check VueUse before writing custom composables - most patterns already implemented.
131
+
132
+ > **For Nuxt-specific composables** (useFetch, useRequestURL): see `nuxt` skill nuxt-composables.md
133
+
134
+ ## Advanced Patterns
135
+
136
+ ### Singleton Composable
137
+
138
+ Share state across all components using the same composable:
139
+
140
+ ```ts
141
+ import { createSharedComposable } from '@vueuse/core'
142
+
143
+ function useMapControlsBase() {
144
+ const mapInstance = ref<Map | null>(null)
145
+ const flyTo = (coords: [number, number]) => mapInstance.value?.flyTo(coords)
146
+ return { mapInstance, flyTo }
147
+ }
148
+
149
+ export const useMapControls = createSharedComposable(useMapControlsBase)
150
+ ```
151
+
152
+ ### Cancellable Fetch with AbortController
153
+
154
+ ```ts
155
+ export function useSearch() {
156
+ let abortController: AbortController | null = null
157
+
158
+ watch(query, async (newQuery) => {
159
+ abortController?.abort()
160
+ abortController = new AbortController()
161
+
162
+ try {
163
+ const data = await $fetch('/api/search', {
164
+ query: { q: newQuery },
165
+ signal: abortController.signal,
166
+ })
167
+ }
168
+ catch (e) {
169
+ if (e.name !== 'AbortError')
170
+ throw e
171
+ }
172
+ })
173
+ }
174
+ ```
175
+
176
+ ### Step-Based State Machine
177
+
178
+ ```ts
179
+ export function useSendFlow() {
180
+ const step = ref<'input' | 'confirm' | 'success'>('input')
181
+ const amount = ref('')
182
+
183
+ const next = () => {
184
+ if (step.value === 'input')
185
+ step.value = 'confirm'
186
+ else if (step.value === 'confirm')
187
+ step.value = 'success'
188
+ }
189
+
190
+ return { step, amount, next }
191
+ }
192
+ ```
193
+
194
+ ### Client-Only Guards
195
+
196
+ ```ts
197
+ export function useUserLocation() {
198
+ const location = ref<GeolocationPosition | null>(null)
199
+
200
+ if (import.meta.client) {
201
+ navigator.geolocation.getCurrentPosition(pos => location.value = pos)
202
+ }
203
+
204
+ return { location }
205
+ }
206
+ ```
207
+
208
+ ### Custom Element Composables (Vue 3.5+)
209
+
210
+ For custom element components, use built-in helpers:
211
+
212
+ ```ts
213
+ import { useHost, useShadowRoot } from 'vue'
214
+
215
+ export function useCustomElement() {
216
+ const host = useHost() // Host element reference
217
+ const shadowRoot = useShadowRoot() // Shadow DOM root
218
+
219
+ onMounted(() => {
220
+ console.log('Host:', host)
221
+ console.log('Shadow:', shadowRoot)
222
+ })
223
+
224
+ return { host, shadowRoot }
225
+ }
226
+ ```
227
+
228
+ **Available in:**
229
+
230
+ - Components using `<script setup>` in custom elements
231
+ - Access via `this.$host` in Options API
232
+
233
+ ### Auto-Save with Debounce
234
+
235
+ ```ts
236
+ export function useAutoSave(content: Ref<string>) {
237
+ const hasChanges = ref(false)
238
+
239
+ const save = useDebounceFn(async () => {
240
+ if (!hasChanges.value)
241
+ return
242
+ await $fetch('/api/save', { method: 'POST', body: { content: content.value } })
243
+ hasChanges.value = false
244
+ }, 1000)
245
+
246
+ watch(content, () => {
247
+ hasChanges.value = true
248
+ save()
249
+ })
250
+
251
+ return { hasChanges }
252
+ }
253
+ ```
254
+
255
+ ### Tagged Logger
256
+
257
+ ```ts
258
+ import { consola } from 'consola'
259
+
260
+ export function useSearch() {
261
+ const logger = consola.withTag('search')
262
+
263
+ watch(query, (q) => {
264
+ logger.info('Query changed:', q)
265
+ })
266
+ }
267
+ ```
268
+
269
+ ## Reactivity Gotchas
270
+
271
+ ### Ref Unwrapping in Reactive
272
+
273
+ Refs auto-unwrap in `reactive()` objects but **NOT** in arrays, Maps, or Sets:
274
+
275
+ ```ts
276
+ // ✅ Object - auto unwraps
277
+ const state = reactive({ count: ref(0) })
278
+ state.count++ // No .value needed
279
+
280
+ // ❌ Array - NO unwrapping
281
+ const arr = reactive([ref(1)])
282
+ arr[0].value // Need .value!
283
+
284
+ // ❌ Map/Set - NO unwrapping
285
+ const map = reactive(new Map([['key', ref(1)]]))
286
+ map.get('key').value // Need .value!
287
+ ```
288
+
289
+ ### watchEffect Conditional Tracking
290
+
291
+ Dependencies inside conditional branches are **not tracked** when condition is false:
292
+
293
+ ```ts
294
+ // ❌ Wrong - dep not tracked when condition false
295
+ watchEffect(() => {
296
+ if (condition.value) {
297
+ console.log(dep.value) // Only tracked when condition=true
298
+ }
299
+ })
300
+
301
+ // ✅ Correct - use explicit watch for conditional deps
302
+ watch([condition, dep], ([cond, d]) => {
303
+ if (cond) console.log(d)
304
+ })
305
+ ```
306
+
307
+ ### Cleanup Patterns
308
+
309
+ **For keep-alive components** - use `onDeactivated`:
310
+
311
+ ```ts
312
+ export function usePolling() {
313
+ let interval: NodeJS.Timeout
314
+
315
+ onMounted(() => { interval = setInterval(poll, 5000) })
316
+ onUnmounted(() => clearInterval(interval))
317
+ onDeactivated(() => clearInterval(interval)) // Pause when deactivated
318
+ onActivated(() => { interval = setInterval(poll, 5000) }) // Resume
319
+ }
320
+ ```
321
+
322
+ **For scope-aware cleanup** - use `tryOnScopeDispose` from VueUse:
323
+
324
+ ```ts
325
+ import { tryOnScopeDispose } from '@vueuse/core'
326
+
327
+ export function useEventSource(url: string) {
328
+ const source = new EventSource(url)
329
+
330
+ // Cleans up when effect scope disposes (component unmount, watcher stop)
331
+ tryOnScopeDispose(() => source.close())
332
+
333
+ return { source }
334
+ }
335
+ ```
336
+
337
+ ## Common Mistakes
338
+
339
+ **Not using `readonly()` for internal state:**
340
+
341
+ ```ts
342
+ // ❌ Wrong - exposes mutable ref
343
+ return { count }
344
+
345
+ // ✅ Correct - prevents external mutation
346
+ return { count: readonly(count) }
347
+ ```
348
+
349
+ **Missing cleanup:**
350
+
351
+ ```ts
352
+ // ❌ Wrong - listener never removed
353
+ onMounted(() => target.addEventListener('click', handler))
354
+
355
+ // ✅ Correct - cleanup on unmount
356
+ onMounted(() => target.addEventListener('click', handler))
357
+ onUnmounted(() => target.removeEventListener('click', handler))
358
+ ```