@pythoughts/vue-skills-mcp 0.1.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 +63 -0
- package/index.mjs +139 -0
- package/package.json +34 -0
- package/skills/create-adaptable-composable/SKILL.md +76 -0
- package/skills/vue-best-practices/SKILL.md +154 -0
- package/skills/vue-best-practices/references/animation-class-based-technique.md +254 -0
- package/skills/vue-best-practices/references/animation-state-driven-technique.md +291 -0
- package/skills/vue-best-practices/references/component-async.md +97 -0
- package/skills/vue-best-practices/references/component-data-flow.md +350 -0
- package/skills/vue-best-practices/references/component-fallthrough-attrs.md +174 -0
- package/skills/vue-best-practices/references/component-keep-alive.md +137 -0
- package/skills/vue-best-practices/references/component-slots.md +216 -0
- package/skills/vue-best-practices/references/component-suspense.md +228 -0
- package/skills/vue-best-practices/references/component-teleport.md +108 -0
- package/skills/vue-best-practices/references/component-transition-group.md +128 -0
- package/skills/vue-best-practices/references/component-transition.md +125 -0
- package/skills/vue-best-practices/references/composables.md +290 -0
- package/skills/vue-best-practices/references/directives.md +162 -0
- package/skills/vue-best-practices/references/perf-avoid-component-abstraction-in-lists.md +159 -0
- package/skills/vue-best-practices/references/perf-v-once-v-memo-directives.md +182 -0
- package/skills/vue-best-practices/references/perf-virtualize-large-lists.md +187 -0
- package/skills/vue-best-practices/references/plugins.md +166 -0
- package/skills/vue-best-practices/references/reactivity.md +346 -0
- package/skills/vue-best-practices/references/render-functions.md +201 -0
- package/skills/vue-best-practices/references/sfc.md +310 -0
- package/skills/vue-best-practices/references/state-management.md +135 -0
- package/skills/vue-best-practices/references/updated-hook-performance.md +187 -0
- package/skills/vue-debug-guides/SKILL.md +205 -0
- package/skills/vue-debug-guides/reference/animation-key-for-rerender.md +160 -0
- package/skills/vue-debug-guides/reference/animation-transitiongroup-performance.md +241 -0
- package/skills/vue-debug-guides/reference/async-component-error-handling.md +115 -0
- package/skills/vue-debug-guides/reference/async-component-keepalive-ref-issue.md +112 -0
- package/skills/vue-debug-guides/reference/async-component-suspense-control.md +84 -0
- package/skills/vue-debug-guides/reference/async-component-vue-router.md +109 -0
- package/skills/vue-debug-guides/reference/attrs-event-listener-merging.md +205 -0
- package/skills/vue-debug-guides/reference/checkbox-true-false-value-form-submission.md +118 -0
- package/skills/vue-debug-guides/reference/cleanup-side-effects.md +172 -0
- package/skills/vue-debug-guides/reference/click-events-on-components.md +180 -0
- package/skills/vue-debug-guides/reference/component-naming-conflicts.md +159 -0
- package/skills/vue-debug-guides/reference/component-ref-requires-defineexpose.md +176 -0
- package/skills/vue-debug-guides/reference/composable-avoid-hidden-side-effects.md +208 -0
- package/skills/vue-debug-guides/reference/composable-call-location-restrictions.md +141 -0
- package/skills/vue-debug-guides/reference/composable-naming-return-pattern.md +139 -0
- package/skills/vue-debug-guides/reference/composable-tovalue-inside-watcheffect.md +182 -0
- package/skills/vue-debug-guides/reference/composition-api-not-functional-programming.md +120 -0
- package/skills/vue-debug-guides/reference/composition-api-script-setup-async-context.md +203 -0
- package/skills/vue-debug-guides/reference/composition-api-vs-react-hooks-differences.md +156 -0
- package/skills/vue-debug-guides/reference/computed-array-mutation.md +148 -0
- package/skills/vue-debug-guides/reference/computed-conditional-dependencies.md +147 -0
- package/skills/vue-debug-guides/reference/computed-no-parameters.md +159 -0
- package/skills/vue-debug-guides/reference/computed-no-side-effects.md +107 -0
- package/skills/vue-debug-guides/reference/computed-return-value-readonly.md +160 -0
- package/skills/vue-debug-guides/reference/configure-app-before-mount.md +89 -0
- package/skills/vue-debug-guides/reference/declare-emits-for-documentation.md +212 -0
- package/skills/vue-debug-guides/reference/define-expose-before-await.md +192 -0
- package/skills/vue-debug-guides/reference/define-model-default-value-sync.md +139 -0
- package/skills/vue-debug-guides/reference/defineEmits-must-be-top-level.md +164 -0
- package/skills/vue-debug-guides/reference/defineEmits-no-runtime-and-type-mixed.md +170 -0
- package/skills/vue-debug-guides/reference/definemodel-object-mutation-no-emit.md +148 -0
- package/skills/vue-debug-guides/reference/dom-update-timing-nexttick.md +90 -0
- package/skills/vue-debug-guides/reference/dynamic-argument-constraints.md +146 -0
- package/skills/vue-debug-guides/reference/dynamic-component-registration-vite.md +147 -0
- package/skills/vue-debug-guides/reference/event-modifier-order-matters.md +101 -0
- package/skills/vue-debug-guides/reference/exact-modifier-for-precise-shortcuts.md +155 -0
- package/skills/vue-debug-guides/reference/fallthrough-attrs-overwrite-vue3.md +159 -0
- package/skills/vue-debug-guides/reference/in-dom-template-parsing-caveats.md +149 -0
- package/skills/vue-debug-guides/reference/inheritattrs-false-for-wrapper-components.md +230 -0
- package/skills/vue-debug-guides/reference/keepalive-router-nested-double-mount.md +222 -0
- package/skills/vue-debug-guides/reference/keepalive-transition-memory-leak.md +144 -0
- package/skills/vue-debug-guides/reference/keyup-modifier-timing.md +137 -0
- package/skills/vue-debug-guides/reference/lifecycle-dom-access-timing.md +216 -0
- package/skills/vue-debug-guides/reference/lifecycle-hooks-synchronous-registration.md +156 -0
- package/skills/vue-debug-guides/reference/lifecycle-ssr-awareness.md +184 -0
- package/skills/vue-debug-guides/reference/local-components-not-in-descendants.md +151 -0
- package/skills/vue-debug-guides/reference/mount-return-value.md +88 -0
- package/skills/vue-debug-guides/reference/multi-root-component-class-attrs.md +93 -0
- package/skills/vue-debug-guides/reference/native-event-collision-with-emits.md +162 -0
- package/skills/vue-debug-guides/reference/no-passive-with-prevent.md +141 -0
- package/skills/vue-debug-guides/reference/no-v-if-with-v-for.md +136 -0
- package/skills/vue-debug-guides/reference/perf-computed-object-stability.md +157 -0
- package/skills/vue-debug-guides/reference/perf-props-stability-update-optimization.md +140 -0
- package/skills/vue-debug-guides/reference/plugin-global-properties-sparingly.md +109 -0
- package/skills/vue-debug-guides/reference/plugin-install-before-mount.md +124 -0
- package/skills/vue-debug-guides/reference/plugin-prefer-provide-inject-over-global-properties.md +120 -0
- package/skills/vue-debug-guides/reference/plugin-typescript-type-augmentation.md +157 -0
- package/skills/vue-debug-guides/reference/prop-defineprops-scope-limitation.md +161 -0
- package/skills/vue-debug-guides/reference/provide-inject-debugging-challenges.md +203 -0
- package/skills/vue-debug-guides/reference/provide-inject-default-value-factory.md +244 -0
- package/skills/vue-debug-guides/reference/provide-inject-reactivity-not-automatic.md +226 -0
- package/skills/vue-debug-guides/reference/provide-inject-synchronous-setup.md +235 -0
- package/skills/vue-debug-guides/reference/reactive-destructuring.md +89 -0
- package/skills/vue-debug-guides/reference/reactivity-debugging-hooks.md +132 -0
- package/skills/vue-debug-guides/reference/reactivity-markraw-for-non-reactive.md +149 -0
- package/skills/vue-debug-guides/reference/reactivity-proxy-identity-hazard.md +96 -0
- package/skills/vue-debug-guides/reference/reactivity-same-tick-batching.md +166 -0
- package/skills/vue-debug-guides/reference/ref-value-access.md +61 -0
- package/skills/vue-debug-guides/reference/refs-in-collections-need-value.md +81 -0
- package/skills/vue-debug-guides/reference/render-function-avoid-internal-vnode-properties.md +151 -0
- package/skills/vue-debug-guides/reference/render-function-vnodes-must-be-unique.md +133 -0
- package/skills/vue-debug-guides/reference/rendering-render-function-h-import-vue3.md +148 -0
- package/skills/vue-debug-guides/reference/rendering-render-function-return-from-setup.md +148 -0
- package/skills/vue-debug-guides/reference/rendering-render-function-slots-as-functions.md +168 -0
- package/skills/vue-debug-guides/reference/rendering-resolve-component-for-string-names.md +231 -0
- package/skills/vue-debug-guides/reference/select-initial-value-ios-bug.md +91 -0
- package/skills/vue-debug-guides/reference/self-referencing-component-name.md +157 -0
- package/skills/vue-debug-guides/reference/sfc-named-exports-forbidden.md +184 -0
- package/skills/vue-debug-guides/reference/sfc-scoped-css-child-component-styling.md +156 -0
- package/skills/vue-debug-guides/reference/sfc-scoped-css-dynamic-content.md +193 -0
- package/skills/vue-debug-guides/reference/sfc-scoped-css-slot-content.md +242 -0
- package/skills/vue-debug-guides/reference/sfc-script-setup-reactivity.md +195 -0
- package/skills/vue-debug-guides/reference/slot-forwarding-to-child-components.md +143 -0
- package/skills/vue-debug-guides/reference/slot-implicit-default-content.md +155 -0
- package/skills/vue-debug-guides/reference/slot-name-reserved-prop.md +109 -0
- package/skills/vue-debug-guides/reference/slot-named-scoped-explicit-default.md +95 -0
- package/skills/vue-debug-guides/reference/slot-render-scope-parent-only.md +135 -0
- package/skills/vue-debug-guides/reference/slot-v-slot-on-components-or-templates-only.md +122 -0
- package/skills/vue-debug-guides/reference/ssr-hydration-mismatch-causes.md +280 -0
- package/skills/vue-debug-guides/reference/ssr-platform-specific-apis.md +256 -0
- package/skills/vue-debug-guides/reference/state-ssr-cross-request-pollution.md +276 -0
- package/skills/vue-debug-guides/reference/suspense-no-builtin-error-handling.md +127 -0
- package/skills/vue-debug-guides/reference/suspense-ssr-hydration-issues.md +159 -0
- package/skills/vue-debug-guides/reference/tailwind-dynamic-class-generation.md +144 -0
- package/skills/vue-debug-guides/reference/teleport-scoped-styles-limitation.md +191 -0
- package/skills/vue-debug-guides/reference/teleport-ssr-hydration.md +152 -0
- package/skills/vue-debug-guides/reference/teleport-target-must-exist.md +113 -0
- package/skills/vue-debug-guides/reference/template-expressions-restrictions.md +114 -0
- package/skills/vue-debug-guides/reference/template-functions-no-side-effects.md +187 -0
- package/skills/vue-debug-guides/reference/template-ref-null-with-v-if.md +123 -0
- package/skills/vue-debug-guides/reference/template-ref-unwrapping-top-level.md +104 -0
- package/skills/vue-debug-guides/reference/template-ref-v-for-order.md +172 -0
- package/skills/vue-debug-guides/reference/textarea-no-interpolation.md +72 -0
- package/skills/vue-debug-guides/reference/transition-group-flip-inline-elements.md +152 -0
- package/skills/vue-debug-guides/reference/transition-group-move-animation-position-absolute.md +130 -0
- package/skills/vue-debug-guides/reference/transition-group-no-default-wrapper-vue3.md +152 -0
- package/skills/vue-debug-guides/reference/transition-js-hooks-done-callback.md +251 -0
- package/skills/vue-debug-guides/reference/transition-nested-duration.md +182 -0
- package/skills/vue-debug-guides/reference/transition-reusable-scoped-style.md +245 -0
- package/skills/vue-debug-guides/reference/transition-router-view-appear.md +193 -0
- package/skills/vue-debug-guides/reference/transition-type-when-mixed.md +172 -0
- package/skills/vue-debug-guides/reference/transition-unmount-hook-timing.md +149 -0
- package/skills/vue-debug-guides/reference/ts-defineprops-boolean-default-false.md +225 -0
- package/skills/vue-debug-guides/reference/ts-defineprops-imported-types-limitations.md +281 -0
- package/skills/vue-debug-guides/reference/ts-event-handler-explicit-typing.md +213 -0
- package/skills/vue-debug-guides/reference/ts-reactive-no-generic-argument.md +196 -0
- package/skills/vue-debug-guides/reference/ts-shallowref-for-dynamic-components.md +218 -0
- package/skills/vue-debug-guides/reference/ts-template-ref-null-handling.md +249 -0
- package/skills/vue-debug-guides/reference/ts-template-type-casting.md +214 -0
- package/skills/vue-debug-guides/reference/ts-withdefaults-mutable-factory-function.md +171 -0
- package/skills/vue-debug-guides/reference/undeclared-emits-double-firing.md +195 -0
- package/skills/vue-debug-guides/reference/use-template-ref-vue35.md +158 -0
- package/skills/vue-debug-guides/reference/v-else-must-follow-v-if.md +136 -0
- package/skills/vue-debug-guides/reference/v-for-component-props.md +95 -0
- package/skills/vue-debug-guides/reference/v-for-computed-reverse-sort.md +86 -0
- package/skills/vue-debug-guides/reference/v-for-key-attribute.md +90 -0
- package/skills/vue-debug-guides/reference/v-for-range-starts-at-one.md +66 -0
- package/skills/vue-debug-guides/reference/v-if-null-check-order.md +171 -0
- package/skills/vue-debug-guides/reference/v-model-ignores-html-attributes.md +83 -0
- package/skills/vue-debug-guides/reference/v-model-ime-composition.md +83 -0
- package/skills/vue-debug-guides/reference/v-model-number-modifier-behavior.md +124 -0
- package/skills/vue-debug-guides/reference/v-show-template-limitation.md +124 -0
- package/skills/vue-debug-guides/reference/watch-async-cleanup.md +180 -0
- package/skills/vue-debug-guides/reference/watch-async-creation-memory-leak.md +176 -0
- package/skills/vue-debug-guides/reference/watch-deep-same-object-reference.md +165 -0
- package/skills/vue-debug-guides/reference/watch-flush-timing.md +189 -0
- package/skills/vue-debug-guides/reference/watch-reactive-property-getter.md +108 -0
- package/skills/vue-debug-guides/reference/watcheffect-async-dependency-tracking.md +173 -0
- package/skills/vue-debug-guides/reference/watcheffect-flush-post-for-refs.md +176 -0
- package/skills/vue-jsx-best-practices/SKILL.md +12 -0
- package/skills/vue-jsx-best-practices/reference/render-function-jsx-vue-vs-react.md +141 -0
- package/skills/vue-options-api-best-practices/SKILL.md +23 -0
- package/skills/vue-options-api-best-practices/reference/no-arrow-functions-in-lifecycle-hooks.md +95 -0
- package/skills/vue-options-api-best-practices/reference/no-arrow-functions-in-methods.md +68 -0
- package/skills/vue-options-api-best-practices/reference/stateful-methods-lifecycle.md +61 -0
- package/skills/vue-options-api-best-practices/reference/ts-options-api-arrow-functions-validators.md +141 -0
- package/skills/vue-options-api-best-practices/reference/ts-options-api-computed-return-types.md +192 -0
- package/skills/vue-options-api-best-practices/reference/ts-options-api-proptype-complex-types.md +212 -0
- package/skills/vue-options-api-best-practices/reference/ts-options-api-provide-inject-limitations.md +135 -0
- package/skills/vue-options-api-best-practices/reference/ts-options-api-type-event-handlers.md +202 -0
- package/skills/vue-options-api-best-practices/reference/ts-options-api-use-definecomponent.md +172 -0
- package/skills/vue-options-api-best-practices/reference/ts-strict-mode-options-api.md +197 -0
- package/skills/vue-pinia-best-practices/SKILL.md +21 -0
- package/skills/vue-pinia-best-practices/reference/pinia-no-active-pinia-error.md +248 -0
- package/skills/vue-pinia-best-practices/reference/pinia-setup-store-return-all-state.md +227 -0
- package/skills/vue-pinia-best-practices/reference/pinia-store-destructuring-breaks-reactivity.md +193 -0
- package/skills/vue-pinia-best-practices/reference/state-url-for-ephemeral-filters.md +238 -0
- package/skills/vue-pinia-best-practices/reference/state-use-pinia-for-large-apps.md +262 -0
- package/skills/vue-pinia-best-practices/reference/store-method-binding-parentheses.md +191 -0
- package/skills/vue-router-best-practices/SKILL.md +23 -0
- package/skills/vue-router-best-practices/reference/router-beforeenter-no-param-trigger.md +167 -0
- package/skills/vue-router-best-practices/reference/router-beforerouteenter-no-this.md +176 -0
- package/skills/vue-router-best-practices/reference/router-guard-async-await-pattern.md +227 -0
- package/skills/vue-router-best-practices/reference/router-navigation-guard-infinite-loop.md +187 -0
- package/skills/vue-router-best-practices/reference/router-navigation-guard-next-deprecated.md +150 -0
- package/skills/vue-router-best-practices/reference/router-param-change-no-lifecycle.md +181 -0
- package/skills/vue-router-best-practices/reference/router-simple-routing-cleanup.md +209 -0
- package/skills/vue-router-best-practices/reference/router-use-vue-router-for-production.md +183 -0
- package/skills/vue-testing-best-practices/SKILL.md +29 -0
- package/skills/vue-testing-best-practices/reference/async-component-testing.md +163 -0
- package/skills/vue-testing-best-practices/reference/teleport-testing-complexity.md +158 -0
- package/skills/vue-testing-best-practices/reference/testing-async-await-flushpromises.md +175 -0
- package/skills/vue-testing-best-practices/reference/testing-browser-vs-node-runners.md +208 -0
- package/skills/vue-testing-best-practices/reference/testing-component-blackbox-approach.md +144 -0
- package/skills/vue-testing-best-practices/reference/testing-composables-helper-wrapper.md +238 -0
- package/skills/vue-testing-best-practices/reference/testing-e2e-playwright-recommended.md +242 -0
- package/skills/vue-testing-best-practices/reference/testing-no-snapshot-only.md +197 -0
- package/skills/vue-testing-best-practices/reference/testing-pinia-store-setup.md +228 -0
- package/skills/vue-testing-best-practices/reference/testing-suspense-async-components.md +229 -0
- package/skills/vue-testing-best-practices/reference/testing-vitest-recommended-for-vue.md +204 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Component Refs Require defineExpose with Script Setup
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: Parent components cannot access child ref properties unless explicitly exposed
|
|
5
|
+
type: gotcha
|
|
6
|
+
tags: [vue3, template-refs, script-setup, defineExpose, component-communication]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Component Refs Require defineExpose with Script Setup
|
|
10
|
+
|
|
11
|
+
**Impact: HIGH** - Components using `<script setup>` are private by default. A parent component using a template ref to access a child will get an empty object unless the child explicitly exposes properties using `defineExpose()`. This is a fundamental change from Options API behavior.
|
|
12
|
+
|
|
13
|
+
This catches many developers off-guard when migrating from Options API, where `this.$refs.child` gave full access to the child instance.
|
|
14
|
+
|
|
15
|
+
## Task Checklist
|
|
16
|
+
|
|
17
|
+
- [ ] Use `defineExpose()` to explicitly expose properties/methods to parent refs
|
|
18
|
+
- [ ] Only expose what's necessary - keep component internals private
|
|
19
|
+
- [ ] Document exposed APIs as they form your component's public interface
|
|
20
|
+
- [ ] Prefer props/emit for parent-child communication; use refs sparingly
|
|
21
|
+
- [ ] Call defineExpose before any await operation (see async caveat)
|
|
22
|
+
|
|
23
|
+
**Incorrect:**
|
|
24
|
+
```vue
|
|
25
|
+
<!-- ChildComponent.vue -->
|
|
26
|
+
<script setup>
|
|
27
|
+
import { ref } from 'vue'
|
|
28
|
+
|
|
29
|
+
const count = ref(0)
|
|
30
|
+
const internalState = ref('private')
|
|
31
|
+
|
|
32
|
+
function increment() {
|
|
33
|
+
count.value++
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function reset() {
|
|
37
|
+
count.value = 0
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// WRONG: Nothing exposed - parent ref sees empty object
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
<template>
|
|
44
|
+
<div>{{ count }}</div>
|
|
45
|
+
</template>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```vue
|
|
49
|
+
<!-- ParentComponent.vue -->
|
|
50
|
+
<script setup>
|
|
51
|
+
import { ref, onMounted } from 'vue'
|
|
52
|
+
import ChildComponent from './ChildComponent.vue'
|
|
53
|
+
|
|
54
|
+
const childRef = ref(null)
|
|
55
|
+
|
|
56
|
+
onMounted(() => {
|
|
57
|
+
// WRONG: childRef.value is {} - empty object!
|
|
58
|
+
console.log(childRef.value.count) // undefined
|
|
59
|
+
childRef.value.increment() // TypeError: not a function
|
|
60
|
+
})
|
|
61
|
+
</script>
|
|
62
|
+
|
|
63
|
+
<template>
|
|
64
|
+
<ChildComponent ref="childRef" />
|
|
65
|
+
</template>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Correct:**
|
|
69
|
+
```vue
|
|
70
|
+
<!-- ChildComponent.vue -->
|
|
71
|
+
<script setup>
|
|
72
|
+
import { ref } from 'vue'
|
|
73
|
+
|
|
74
|
+
const count = ref(0)
|
|
75
|
+
const internalState = ref('private') // Keep this private
|
|
76
|
+
|
|
77
|
+
function increment() {
|
|
78
|
+
count.value++
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function reset() {
|
|
82
|
+
count.value = 0
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// CORRECT: Explicitly expose public API
|
|
86
|
+
defineExpose({
|
|
87
|
+
count, // Expose the ref
|
|
88
|
+
increment, // Expose methods
|
|
89
|
+
reset
|
|
90
|
+
// internalState NOT exposed - stays private
|
|
91
|
+
})
|
|
92
|
+
</script>
|
|
93
|
+
|
|
94
|
+
<template>
|
|
95
|
+
<div>{{ count }}</div>
|
|
96
|
+
</template>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
```vue
|
|
100
|
+
<!-- ParentComponent.vue -->
|
|
101
|
+
<script setup>
|
|
102
|
+
import { ref, onMounted } from 'vue'
|
|
103
|
+
import ChildComponent from './ChildComponent.vue'
|
|
104
|
+
|
|
105
|
+
const childRef = ref(null)
|
|
106
|
+
|
|
107
|
+
onMounted(() => {
|
|
108
|
+
// CORRECT: Can access exposed properties
|
|
109
|
+
console.log(childRef.value.count) // 0
|
|
110
|
+
childRef.value.increment() // Works!
|
|
111
|
+
|
|
112
|
+
// internalState is not accessible (private)
|
|
113
|
+
console.log(childRef.value.internalState) // undefined
|
|
114
|
+
})
|
|
115
|
+
</script>
|
|
116
|
+
|
|
117
|
+
<template>
|
|
118
|
+
<ChildComponent ref="childRef" />
|
|
119
|
+
</template>
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
```vue
|
|
123
|
+
<!-- Input wrapper example - exposing native element -->
|
|
124
|
+
<script setup>
|
|
125
|
+
import { ref } from 'vue'
|
|
126
|
+
|
|
127
|
+
const inputEl = ref(null)
|
|
128
|
+
|
|
129
|
+
// Expose the native input for parent to access (e.g., for focus)
|
|
130
|
+
defineExpose({
|
|
131
|
+
focus: () => inputEl.value?.focus(),
|
|
132
|
+
blur: () => inputEl.value?.blur(),
|
|
133
|
+
// Or expose the element directly
|
|
134
|
+
el: inputEl
|
|
135
|
+
})
|
|
136
|
+
</script>
|
|
137
|
+
|
|
138
|
+
<template>
|
|
139
|
+
<input ref="inputEl" v-bind="$attrs" />
|
|
140
|
+
</template>
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
```javascript
|
|
144
|
+
// Options API equivalent using expose option
|
|
145
|
+
export default {
|
|
146
|
+
expose: ['count', 'increment', 'reset'],
|
|
147
|
+
data() {
|
|
148
|
+
return {
|
|
149
|
+
count: 0,
|
|
150
|
+
internalState: 'private'
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
methods: {
|
|
154
|
+
increment() { this.count++ },
|
|
155
|
+
reset() { this.count = 0 }
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Best Practice Reminder
|
|
161
|
+
|
|
162
|
+
Component refs create tight coupling between parent and child. Prefer standard patterns:
|
|
163
|
+
|
|
164
|
+
```vue
|
|
165
|
+
<!-- PREFERRED: Use props and emit for communication -->
|
|
166
|
+
<script setup>
|
|
167
|
+
const props = defineProps(['modelValue'])
|
|
168
|
+
const emit = defineEmits(['update:modelValue'])
|
|
169
|
+
</script>
|
|
170
|
+
|
|
171
|
+
<!-- Only use refs for imperative actions like focus(), scrollTo(), etc. -->
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Reference
|
|
175
|
+
- [Vue.js Component Refs](https://vuejs.org/guide/essentials/template-refs.html#ref-on-component)
|
|
176
|
+
- [Script Setup - defineExpose](https://vuejs.org/api/sfc-script-setup.html#defineexpose)
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Avoid Hidden Side Effects in Composables
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: Side effects hidden in composables make debugging difficult and create implicit coupling between components
|
|
5
|
+
type: best-practice
|
|
6
|
+
tags: [vue3, composables, composition-api, side-effects, provide-inject, global-state]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Avoid Hidden Side Effects in Composables
|
|
10
|
+
|
|
11
|
+
**Impact: HIGH** - Composables should encapsulate stateful logic, not hide side effects that affect things outside their scope. Hidden side effects like modifying global state, using provide/inject internally, or manipulating the DOM directly make composables unpredictable and hard to debug.
|
|
12
|
+
|
|
13
|
+
When a composable has unexpected side effects, consumers can't reason about what calling it will do. This leads to bugs that are difficult to trace and composables that can't be safely reused.
|
|
14
|
+
|
|
15
|
+
## Task Checklist
|
|
16
|
+
|
|
17
|
+
- [ ] Avoid using provide/inject inside composables (make dependencies explicit)
|
|
18
|
+
- [ ] Don't modify Pinia/Vuex store state internally (accept store as parameter instead)
|
|
19
|
+
- [ ] Don't manipulate DOM directly (use template refs passed as arguments)
|
|
20
|
+
- [ ] Document any unavoidable side effects clearly
|
|
21
|
+
- [ ] Keep composables focused on returning reactive state and methods
|
|
22
|
+
|
|
23
|
+
**Incorrect:**
|
|
24
|
+
```javascript
|
|
25
|
+
// WRONG: Hidden provide/inject dependency
|
|
26
|
+
export function useTheme() {
|
|
27
|
+
// Consumer has no idea this depends on a provided theme
|
|
28
|
+
const theme = inject('theme') // What if nothing provides this?
|
|
29
|
+
|
|
30
|
+
const isDark = computed(() => theme?.mode === 'dark')
|
|
31
|
+
return { isDark }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// WRONG: Modifying global store internally
|
|
35
|
+
import { useUserStore } from '@/stores/user'
|
|
36
|
+
|
|
37
|
+
export function useLogin() {
|
|
38
|
+
const userStore = useUserStore()
|
|
39
|
+
|
|
40
|
+
async function login(credentials) {
|
|
41
|
+
const user = await api.login(credentials)
|
|
42
|
+
// Hidden side effect: modifying global state
|
|
43
|
+
userStore.setUser(user)
|
|
44
|
+
userStore.setToken(user.token)
|
|
45
|
+
// Consumer doesn't know the store was modified!
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return { login }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// WRONG: Hidden DOM manipulation
|
|
52
|
+
export function useFocusTrap() {
|
|
53
|
+
onMounted(() => {
|
|
54
|
+
// Which element? Consumer has no control
|
|
55
|
+
document.querySelector('.modal')?.focus()
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// WRONG: Hidden provide that affects descendants
|
|
60
|
+
export function useFormContext() {
|
|
61
|
+
const form = reactive({ values: {}, errors: {} })
|
|
62
|
+
// Components calling this have no idea it provides something
|
|
63
|
+
provide('form-context', form)
|
|
64
|
+
return form
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Correct:**
|
|
69
|
+
```javascript
|
|
70
|
+
// CORRECT: Explicit dependency injection
|
|
71
|
+
export function useTheme(injectedTheme) {
|
|
72
|
+
// If no theme passed, consumer must handle it
|
|
73
|
+
const theme = injectedTheme ?? { mode: 'light' }
|
|
74
|
+
|
|
75
|
+
const isDark = computed(() => theme.mode === 'dark')
|
|
76
|
+
return { isDark }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Usage - dependency is explicit
|
|
80
|
+
const theme = inject('theme', { mode: 'light' })
|
|
81
|
+
const { isDark } = useTheme(theme)
|
|
82
|
+
|
|
83
|
+
// CORRECT: Return actions, let consumer decide when to call them
|
|
84
|
+
export function useLogin() {
|
|
85
|
+
const user = ref(null)
|
|
86
|
+
const token = ref(null)
|
|
87
|
+
const isLoading = ref(false)
|
|
88
|
+
const error = ref(null)
|
|
89
|
+
|
|
90
|
+
async function login(credentials) {
|
|
91
|
+
isLoading.value = true
|
|
92
|
+
error.value = null
|
|
93
|
+
try {
|
|
94
|
+
const response = await api.login(credentials)
|
|
95
|
+
user.value = response.user
|
|
96
|
+
token.value = response.token
|
|
97
|
+
return response
|
|
98
|
+
} catch (e) {
|
|
99
|
+
error.value = e
|
|
100
|
+
throw e
|
|
101
|
+
} finally {
|
|
102
|
+
isLoading.value = false
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return { user, token, isLoading, error, login }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Consumer decides what to do with the result
|
|
110
|
+
const { user, token, login } = useLogin()
|
|
111
|
+
const userStore = useUserStore()
|
|
112
|
+
|
|
113
|
+
async function handleLogin(credentials) {
|
|
114
|
+
await login(credentials)
|
|
115
|
+
// Consumer explicitly updates the store
|
|
116
|
+
userStore.setUser(user.value)
|
|
117
|
+
userStore.setToken(token.value)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// CORRECT: Accept element as parameter
|
|
121
|
+
export function useFocusTrap(targetRef) {
|
|
122
|
+
onMounted(() => {
|
|
123
|
+
targetRef.value?.focus()
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
onUnmounted(() => {
|
|
127
|
+
// Cleanup focus trap
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Usage - consumer controls which element
|
|
132
|
+
const modalRef = ref(null)
|
|
133
|
+
useFocusTrap(modalRef)
|
|
134
|
+
|
|
135
|
+
// CORRECT: Separate composable from provider
|
|
136
|
+
export function useFormContext() {
|
|
137
|
+
const form = reactive({ values: {}, errors: {} })
|
|
138
|
+
return form
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// In parent component - explicit provide
|
|
142
|
+
const form = useFormContext()
|
|
143
|
+
provide('form-context', form)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Acceptable Side Effects (With Documentation)
|
|
147
|
+
|
|
148
|
+
Some side effects are acceptable when they're the core purpose of the composable:
|
|
149
|
+
|
|
150
|
+
```javascript
|
|
151
|
+
/**
|
|
152
|
+
* Tracks mouse position globally.
|
|
153
|
+
*
|
|
154
|
+
* SIDE EFFECTS:
|
|
155
|
+
* - Adds 'mousemove' event listener to window (cleaned up on unmount)
|
|
156
|
+
*
|
|
157
|
+
* @returns {Object} Mouse coordinates { x, y }
|
|
158
|
+
*/
|
|
159
|
+
export function useMouse() {
|
|
160
|
+
const x = ref(0)
|
|
161
|
+
const y = ref(0)
|
|
162
|
+
|
|
163
|
+
// This side effect is the whole point of the composable
|
|
164
|
+
// and is properly cleaned up
|
|
165
|
+
onMounted(() => window.addEventListener('mousemove', update))
|
|
166
|
+
onUnmounted(() => window.removeEventListener('mousemove', update))
|
|
167
|
+
|
|
168
|
+
function update(event) {
|
|
169
|
+
x.value = event.pageX
|
|
170
|
+
y.value = event.pageY
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return { x, y }
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Pattern: Dependency Injection for Flexibility
|
|
178
|
+
|
|
179
|
+
```javascript
|
|
180
|
+
// Composable accepts its dependencies
|
|
181
|
+
export function useDataFetcher(apiClient, cache = null) {
|
|
182
|
+
const data = ref(null)
|
|
183
|
+
|
|
184
|
+
async function fetch(url) {
|
|
185
|
+
if (cache) {
|
|
186
|
+
const cached = cache.get(url)
|
|
187
|
+
if (cached) {
|
|
188
|
+
data.value = cached
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
data.value = await apiClient.get(url)
|
|
194
|
+
cache?.set(url, data.value)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return { data, fetch }
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Usage - dependencies are explicit and testable
|
|
201
|
+
const apiClient = inject('apiClient')
|
|
202
|
+
const cache = inject('cache', null)
|
|
203
|
+
const { data, fetch } = useDataFetcher(apiClient, cache)
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Reference
|
|
207
|
+
- [Vue.js Composables](https://vuejs.org/guide/reusability/composables.html)
|
|
208
|
+
- [Common Mistakes Creating Composition Functions](https://www.telerik.com/blogs/common-mistakes-creating-composition-functions-vue)
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Call Composables Only in Setup Context Synchronously
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: Composables called outside setup context or asynchronously fail to register lifecycle hooks and may cause memory leaks
|
|
5
|
+
type: gotcha
|
|
6
|
+
tags: [vue3, composables, composition-api, setup, async, lifecycle]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Call Composables Only in Setup Context Synchronously
|
|
10
|
+
|
|
11
|
+
**Impact: HIGH** - Composables must be called synchronously within `<script setup>`, the `setup()` function, or lifecycle hooks. Calling composables asynchronously (after await), in callbacks, or outside component context prevents Vue from associating lifecycle hooks with the component instance, causing silent failures.
|
|
12
|
+
|
|
13
|
+
This is critical because composables often register `onMounted` and `onUnmounted` hooks internally. If called in the wrong context, these hooks are never registered, leading to uninitialized state or memory leaks.
|
|
14
|
+
|
|
15
|
+
## Task Checklist
|
|
16
|
+
|
|
17
|
+
- [ ] Call all composables at the top level of `<script setup>` or `setup()`
|
|
18
|
+
- [ ] Never call composables inside async callbacks, setTimeout, or Promise.then
|
|
19
|
+
- [ ] Never call composables conditionally (if/else) - call unconditionally and handle the condition inside
|
|
20
|
+
- [ ] Never call composables inside loops - restructure to call once with array data
|
|
21
|
+
- [ ] Exception: Composables CAN be called in lifecycle hooks like `onMounted`
|
|
22
|
+
|
|
23
|
+
**Incorrect:**
|
|
24
|
+
```vue
|
|
25
|
+
<script setup>
|
|
26
|
+
import { useFetch } from './composables/useFetch'
|
|
27
|
+
import { useAuth } from './composables/useAuth'
|
|
28
|
+
|
|
29
|
+
// WRONG: Composable called after await
|
|
30
|
+
const config = await loadConfig()
|
|
31
|
+
const { data } = useFetch(config.apiUrl) // Lifecycle hooks won't register!
|
|
32
|
+
|
|
33
|
+
// WRONG: Composable called conditionally
|
|
34
|
+
if (someCondition) {
|
|
35
|
+
const { user } = useAuth() // Inconsistent hook registration!
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// WRONG: Composable called in callback
|
|
39
|
+
setTimeout(() => {
|
|
40
|
+
const { data } = useFetch('/api/delayed') // No component context!
|
|
41
|
+
}, 1000)
|
|
42
|
+
|
|
43
|
+
// WRONG: Composable called in loop
|
|
44
|
+
for (const url of urls) {
|
|
45
|
+
const { data } = useFetch(url) // Creates multiple instances incorrectly
|
|
46
|
+
}
|
|
47
|
+
</script>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Correct:**
|
|
51
|
+
```vue
|
|
52
|
+
<script setup>
|
|
53
|
+
import { ref, onMounted } from 'vue'
|
|
54
|
+
import { useFetch } from './composables/useFetch'
|
|
55
|
+
import { useAuth } from './composables/useAuth'
|
|
56
|
+
|
|
57
|
+
// CORRECT: Call composables synchronously at top level
|
|
58
|
+
const { user, isAuthenticated } = useAuth()
|
|
59
|
+
const apiUrl = ref('/api/default')
|
|
60
|
+
const { data, execute } = useFetch(apiUrl)
|
|
61
|
+
|
|
62
|
+
// Handle async config loading differently
|
|
63
|
+
onMounted(async () => {
|
|
64
|
+
const config = await loadConfig()
|
|
65
|
+
apiUrl.value = config.apiUrl // Update the ref, composable reacts
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
// CORRECT: Handle condition inside, not outside
|
|
69
|
+
const showUserData = computed(() => isAuthenticated.value && someCondition)
|
|
70
|
+
|
|
71
|
+
// CORRECT: For multiple URLs, use a different pattern
|
|
72
|
+
const urls = ref(['/api/a', '/api/b', '/api/c'])
|
|
73
|
+
const results = ref([])
|
|
74
|
+
|
|
75
|
+
// Either fetch in onMounted or use a composable designed for arrays
|
|
76
|
+
onMounted(async () => {
|
|
77
|
+
results.value = await Promise.all(urls.value.map(url => fetch(url)))
|
|
78
|
+
})
|
|
79
|
+
</script>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Exception: Calling in Lifecycle Hooks
|
|
83
|
+
|
|
84
|
+
Composables CAN be called inside lifecycle hooks because Vue maintains the component context:
|
|
85
|
+
|
|
86
|
+
```vue
|
|
87
|
+
<script setup>
|
|
88
|
+
import { onMounted } from 'vue'
|
|
89
|
+
import { useEventListener } from '@vueuse/core'
|
|
90
|
+
|
|
91
|
+
// CORRECT: Called in lifecycle hook - component context is available
|
|
92
|
+
onMounted(() => {
|
|
93
|
+
// This works because we're still in the component's execution context
|
|
94
|
+
useEventListener(document, 'visibilitychange', handleVisibility)
|
|
95
|
+
})
|
|
96
|
+
</script>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Special Case: Async Setup in `<script setup>`
|
|
100
|
+
|
|
101
|
+
Top-level await in `<script setup>` is special - Vue's compiler automatically preserves context:
|
|
102
|
+
|
|
103
|
+
```vue
|
|
104
|
+
<script setup>
|
|
105
|
+
import { useFetch } from './composables/useFetch'
|
|
106
|
+
|
|
107
|
+
// CORRECT: Top-level await in <script setup> preserves context
|
|
108
|
+
// Vue compiler handles this specially
|
|
109
|
+
const config = await loadConfig()
|
|
110
|
+
const { data } = useFetch(config.apiUrl) // This works!
|
|
111
|
+
|
|
112
|
+
// But nested awaits still break context:
|
|
113
|
+
async function initLater() {
|
|
114
|
+
await delay(1000)
|
|
115
|
+
const { data } = useFetch('/api/late') // WRONG: This won't work!
|
|
116
|
+
}
|
|
117
|
+
</script>
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Why This Matters
|
|
121
|
+
|
|
122
|
+
When you call a composable, Vue needs to know which component instance to associate it with. This association happens through an internal "current instance" that's only set during synchronous setup execution.
|
|
123
|
+
|
|
124
|
+
```javascript
|
|
125
|
+
// Inside a composable
|
|
126
|
+
export function useFetch(url) {
|
|
127
|
+
const data = ref(null)
|
|
128
|
+
|
|
129
|
+
// These need the current component instance!
|
|
130
|
+
onMounted(() => { /* ... */ })
|
|
131
|
+
onUnmounted(() => { /* cleanup */ })
|
|
132
|
+
|
|
133
|
+
// If called outside setup context, Vue can't find the instance
|
|
134
|
+
// and these hooks are silently ignored
|
|
135
|
+
return { data }
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Reference
|
|
140
|
+
- [Vue.js Composables - Usage Restrictions](https://vuejs.org/guide/reusability/composables.html#usage-restrictions)
|
|
141
|
+
- [Vue.js Composition API - Setup Context](https://vuejs.org/api/composition-api-setup.html)
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Follow Composable Naming Convention and Return Pattern
|
|
3
|
+
impact: MEDIUM
|
|
4
|
+
impactDescription: Inconsistent composable patterns lead to confusing APIs and reactivity issues when destructuring
|
|
5
|
+
type: best-practice
|
|
6
|
+
tags: [vue3, composables, composition-api, naming, conventions, refs]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Follow Composable Naming Convention and Return Pattern
|
|
10
|
+
|
|
11
|
+
**Impact: MEDIUM** - Vue composables should follow established conventions: prefix names with "use" and return plain objects containing refs (not reactive objects). Returning reactive objects causes reactivity loss when destructuring, while inconsistent naming makes code harder to understand.
|
|
12
|
+
|
|
13
|
+
## Task Checklist
|
|
14
|
+
|
|
15
|
+
- [ ] Name composables with "use" prefix (e.g., `useMouse`, `useFetch`, `useAuth`)
|
|
16
|
+
- [ ] Return a plain object containing refs, not a reactive object
|
|
17
|
+
- [ ] Allow both destructuring and object-style access
|
|
18
|
+
- [ ] Document the returned refs for consumers
|
|
19
|
+
|
|
20
|
+
**Incorrect:**
|
|
21
|
+
```javascript
|
|
22
|
+
// WRONG: No "use" prefix - unclear it's a composable
|
|
23
|
+
export function mousePosition() {
|
|
24
|
+
const x = ref(0)
|
|
25
|
+
const y = ref(0)
|
|
26
|
+
return { x, y }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// WRONG: Returning reactive object - destructuring loses reactivity
|
|
30
|
+
export function useMouse() {
|
|
31
|
+
const state = reactive({
|
|
32
|
+
x: 0,
|
|
33
|
+
y: 0
|
|
34
|
+
})
|
|
35
|
+
// When consumer destructures: const { x, y } = useMouse()
|
|
36
|
+
// x and y become plain values, not reactive!
|
|
37
|
+
return state
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// WRONG: Returning single ref directly - inconsistent API
|
|
41
|
+
export function useCounter() {
|
|
42
|
+
const count = ref(0)
|
|
43
|
+
return count // Consumer must use .value everywhere
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Correct:**
|
|
48
|
+
```javascript
|
|
49
|
+
// CORRECT: "use" prefix and returns plain object with refs
|
|
50
|
+
export function useMouse() {
|
|
51
|
+
const x = ref(0)
|
|
52
|
+
const y = ref(0)
|
|
53
|
+
|
|
54
|
+
function update(event) {
|
|
55
|
+
x.value = event.pageX
|
|
56
|
+
y.value = event.pageY
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
onMounted(() => window.addEventListener('mousemove', update))
|
|
60
|
+
onUnmounted(() => window.removeEventListener('mousemove', update))
|
|
61
|
+
|
|
62
|
+
// Return plain object containing refs
|
|
63
|
+
return { x, y }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Consumer can destructure and keep reactivity
|
|
67
|
+
const { x, y } = useMouse()
|
|
68
|
+
watch(x, (newX) => console.log('x changed:', newX)) // Works!
|
|
69
|
+
|
|
70
|
+
// Or use as object if preferred
|
|
71
|
+
const mouse = useMouse()
|
|
72
|
+
console.log(mouse.x.value)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Using reactive() Wrapper for Auto-Unwrapping
|
|
76
|
+
|
|
77
|
+
If consumers prefer auto-unwrapping (no `.value`), they can wrap the result:
|
|
78
|
+
|
|
79
|
+
```javascript
|
|
80
|
+
import { reactive } from 'vue'
|
|
81
|
+
import { useMouse } from './composables/useMouse'
|
|
82
|
+
|
|
83
|
+
// Wrapping in reactive() links the refs
|
|
84
|
+
const mouse = reactive(useMouse())
|
|
85
|
+
|
|
86
|
+
// Now access without .value
|
|
87
|
+
console.log(mouse.x) // Auto-unwrapped, still reactive
|
|
88
|
+
|
|
89
|
+
// But DON'T destructure from this!
|
|
90
|
+
const { x } = reactive(useMouse()) // WRONG: loses reactivity again
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Pattern: Returning Both State and Actions
|
|
94
|
+
|
|
95
|
+
```javascript
|
|
96
|
+
// Composable with state AND methods
|
|
97
|
+
export function useCounter(initialValue = 0) {
|
|
98
|
+
const count = ref(initialValue)
|
|
99
|
+
const doubleCount = computed(() => count.value * 2)
|
|
100
|
+
|
|
101
|
+
function increment() {
|
|
102
|
+
count.value++
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function decrement() {
|
|
106
|
+
count.value--
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function reset() {
|
|
110
|
+
count.value = initialValue
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Return all refs and functions in plain object
|
|
114
|
+
return {
|
|
115
|
+
count,
|
|
116
|
+
doubleCount,
|
|
117
|
+
increment,
|
|
118
|
+
decrement,
|
|
119
|
+
reset
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Usage
|
|
124
|
+
const { count, doubleCount, increment, reset } = useCounter(10)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Naming Convention Examples
|
|
128
|
+
|
|
129
|
+
| Good Name | Bad Name | Reason |
|
|
130
|
+
|-----------|----------|--------|
|
|
131
|
+
| `useFetch` | `fetch` | Conflicts with native fetch |
|
|
132
|
+
| `useAuth` | `authStore` | "Store" implies Pinia/Vuex |
|
|
133
|
+
| `useLocalStorage` | `localStorage` | Conflicts with native API |
|
|
134
|
+
| `useFormValidation` | `validateForm` | Sounds like a one-shot function |
|
|
135
|
+
| `useWindowSize` | `getWindowSize` | "get" implies synchronous getter |
|
|
136
|
+
|
|
137
|
+
## Reference
|
|
138
|
+
- [Vue.js Composables - Conventions and Best Practices](https://vuejs.org/guide/reusability/composables.html#conventions-and-best-practices)
|
|
139
|
+
- [Vue.js Composables - Return Values](https://vuejs.org/guide/reusability/composables.html#return-values)
|