Vue 3 reactivity and component design
Knowing ref and computed is only the starting point. On a complex page, the real questions are where state belongs, who is allowed to update it, when dependencies are released, and whether deep reactivity is worth its runtime cost.
The reactivity model
Vue tracks the active effect when reactive data is read and triggers the relevant effects when it is written. reactive wraps objects with a Proxy, while ref provides a consistent .value container for primitives and objects. Templates unwrap refs automatically.
import { computed, reactive, ref, shallowRef } from 'vue'
const query = reactive({ keyword: '', page: 1 })
const rows = shallowRef<Row[]>([])
const selectedIds = ref(new Set<string>())
const selectedCount = computed(() => selectedIds.value.size)shallowRef is appropriate for a large server response when the application replaces the whole array and does not need deep fields to be proxied. This should still be based on a Vue DevTools or Performance profile, not intuition alone.
Choosing the right primitive
- Prefer
reffor independent values and objects that may be replaced as a whole. reactiveworks well for an aggregate form, but do not destructure it directly; usetoRefswhen needed.- Use
shallowReformarkRawfor large read-only structures and third-party instances. - Use
computedfor derived state instead of synchronizing a second copy throughwatch.
Watchers are for side effects
Watchers are appropriate for requests, persistence, and third-party SDK calls. Fast-changing input creates races, so stale work must be cancelled.
watch(
() => query.keyword,
async (keyword, _, onCleanup) => {
const controller = new AbortController()
onCleanup(() => controller.abort())
rows.value = await search(keyword, { signal: controller.signal })
},
)Avoid watching the complete route object or using unrestricted deep: true on large structures. Watching a specific parameter makes both the cost and the trigger condition easier to reason about and test.
Extract components along axes of change
I typically separate base UI components, domain components, and page orchestration. A shared component needs a stable reuse case, an explicit variation axis, and a testable contract.
<ProductSelector
v-model="selectedIds"
:source="productSource"
:permission="permission"
@confirm="handleConfirm"
>
<template #empty>No products match the current filters.</template>
</ProductSelector>Props and emits define the data and behavior contract; slots handle structural variation. If two use cases have different domain semantics and lifecycles, two focused domain components are often clearer than one component with dozens of boolean props.
Performance diagnosis order
- Determine whether the bottleneck is network, JavaScript, layout, or component updates.
- Use Vue DevTools for repeated updates and dependency scope; use Performance for long tasks and layout work.
- Apply virtualization, stable keys and props, or lazy loading only where the data supports it.
- Compare the same interaction and data volume before and after the change.
References: Reactivity in Depth, Watchers, and Performance Best Practices.