Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Enforce Vue 3 best practices—Composition API, script setup, TypeScript, component boundaries, and reactivity patterns
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/reactivity.md
1---2title: Reactivity Core Patterns (ref, reactive, shallowRef, computed, watch)3impact: MEDIUM4impactDescription: Clear reactivity choices keep state predictable and reduce unnecessary updates in Vue 3 apps5type: efficiency6tags: [vue3, reactivity, ref, reactive, shallowRef, computed, watch, watchEffect, external-state, best-practice]7---89# Reactivity Core Patterns (ref, reactive, shallowRef, computed, watch)1011**Impact: MEDIUM** - Choose the right reactive primitive first, derive with `computed`, and use watchers only for side effects.1213This reference covers the core reactivity decisions for local state, external data, derived values, and effects.1415## Task List1617- Declare reactive state correctly18- Always use `shallowRef()` instead of `ref()` for primitive values19- Choose the correct reactive declaration method for objects/arrays/map/set20- Follow best practices for `reactive`21- Avoid destructuring from `reactive()` directly22- Watch correctly for `reactive`23- Follow best practices for `computed`24- Prefer `computed` over watcher-assigned derived refs25- Keep filtered/sorted derivations out of templates26- Use `computed` for reusable class/style logic27- Keep computed getters pure (no side effects) and put side effects in watchers28- Follow best practices for watchers29- Use `immediate: true` instead of duplicate initial calls30- Clean up async effects for watchers3132## Declare reactive state correctly3334### Always use `shallowRef()` instead of `ref()` for primitive values (string, number, boolean, null, etc.) for better performance.3536**Incorrect:**37```ts38import { ref } from 'vue'39const count = ref(0)40```4142**Correct:**43```ts44import { shallowRef } from 'vue'45const count = shallowRef(0)46```4748### Choose the correct reactive declaration method for objects/arrays/map/set4950Use `ref()` when you often **replace the entire value** (`state.value = newObj`) and still want deep reactivity inside it, usually used for:5152- Frequently reassigned state (replace fetched object/list, reset to defaults, switch presets).53- Composable return values where updates happen mostly via `.value` reassignment.5455Use `reactive()` when you mainly **mutate properties** and full replacement is uncommon, usually used for:5657- “Single state object” patterns (stores/forms): `state.count++`, `state.items.push(...)`, `state.user.name = ...`.58- Situations where you want to avoid `.value` and update nested fields in place.5960```ts61import { reactive } from 'vue'6263const state = reactive({64count: 0,65user: { name: 'Alice', age: 30 }66})6768state.count++ // ✅ reactive69state.user.age = 31 // ✅ reactive70// ❌ avoid replacing the reactive object reference:71// state = reactive({ count: 1 })72```7374Use `shallowRef()` when the value is **opaque / should not be proxied** (class instances, external library objects, very large nested data) and you only want updates to trigger when you **replace** `state.value` (no deep tracking), usually used for:7576- Storing external instances/handles (SDK clients, class instances) without Vue proxying internals.77- Large data where you update by replacing the root reference (immutable-style updates).7879```ts80import { shallowRef } from 'vue'8182const user = shallowRef({ name: 'Alice', age: 30 })8384user.value.age = 31 // ❌ not reactive85user.value = { name: 'Bob', age: 25 } // ✅ triggers update86```8788Use `shallowReactive()` when you want **only top-level properties** reactive; nested objects remain raw, usually used for:8990- Container objects where only top-level keys change and nested payloads should stay unmanaged/unproxied.91- Mixed structures where Vue tracks the wrapper object, but not deeply nested or foreign objects.9293```ts94import { shallowReactive } from 'vue'9596const state = shallowReactive({97count: 0,98user: { name: 'Alice', age: 30 }99})100101state.count++ // ✅ reactive102state.user.age = 31 // ❌ not reactive103```104105## Best practices for `reactive`106107### Avoid destructuring from `reactive()` directly108109**BAD:**110111```ts112import { reactive } from 'vue'113114const state = reactive({ count: 0 })115const { count } = state // ❌ disconnected from reactivity116```117118### Watch correctly for reactive119120**BAD:**121122passing a non-getter value into `watch()`123124```ts125import { reactive, watch } from 'vue'126127const state = reactive({ count: 0 })128129// ❌ watch expects a getter, ref, reactive object, or array of these130watch(state.count, () => { /* ... */ })131```132133**GOOD:**134135preserve reactivity with `toRefs()` and use a getter for `watch()`136137```ts138import { reactive, toRefs, watch } from 'vue'139140const state = reactive({ count: 0 })141const { count } = toRefs(state) // ✅ count is a ref142143watch(count, () => { /* ... */ }) // ✅144watch(() => state.count, () => { /* ... */ }) // ✅145```146147## Best practices for `computed`148149### Prefer `computed` over watcher-assigned derived refs150151**BAD:**152```ts153import { ref, watchEffect } from 'vue'154155const items = ref([{ price: 10 }, { price: 20 }])156const total = ref(0)157158watchEffect(() => {159total.value = items.value.reduce((sum, item) => sum + item.price, 0)160})161```162163**GOOD:**164```ts165import { ref, computed } from 'vue'166167const items = ref([{ price: 10 }, { price: 20 }])168const total = computed(() =>169items.value.reduce((sum, item) => sum + item.price, 0)170)171```172173### Keep filtered/sorted derivations out of templates174175**BAD:**176```vue177<template>178<li v-for="item in items.filter(item => item.active)" :key="item.id">179{{ item.name }}180</li>181182<li v-for="item in getSortedItems()" :key="item.id">183{{ item.name }}184</li>185</template>186187<script setup>188import { ref } from 'vue'189190const items = ref([191{ id: 1, name: 'B', active: true },192{ id: 2, name: 'A', active: false }193])194195function getSortedItems() {196return [...items.value].sort((a, b) => a.name.localeCompare(b.name))197}198</script>199```200201**GOOD:**202```vue203<script setup>204import { ref, computed } from 'vue'205206const items = ref([207{ id: 1, name: 'B', active: true },208{ id: 2, name: 'A', active: false }209])210211const visibleItems = computed(() =>212items.value213.filter(item => item.active)214.sort((a, b) => a.name.localeCompare(b.name))215)216</script>217218<template>219<li v-for="item in visibleItems" :key="item.id">220{{ item.name }}221</li>222</template>223```224225### Use `computed` for reusable class/style logic226227**BAD:**228```vue229<template>230<button :class="{ btn: true, 'btn-primary': type === 'primary' && !disabled, 'btn-disabled': disabled }">231{{ label }}232</button>233</template>234```235236**GOOD:**237```vue238<script setup>239import { computed } from 'vue'240241const props = defineProps({242type: { type: String, default: 'primary' },243disabled: Boolean,244label: String245})246247const buttonClasses = computed(() => ({248btn: true,249[`btn-${props.type}`]: !props.disabled,250'btn-disabled': props.disabled251}))252</script>253254<template>255<button :class="buttonClasses">256{{ label }}257</button>258</template>259```260261### Keep computed getters pure (no side effects) and put side effects in watchers instead262263A computed getter should only derive a value. No mutation, no API calls, no storage writes, no event emits.264([Reference](https://vuejs.org/guide/essentials/computed.html#best-practices))265266**BAD:**267268side effects inside computed269270```ts271const count = ref(0)272273const doubled = computed(() => {274// ❌ side effect275if (count.value > 10) console.warn('Too big!')276return count.value * 2277})278```279280**GOOD:**281282pure computed + `watch()` for side effects283284```ts285const count = ref(0)286const doubled = computed(() => count.value * 2)287288watch(count, (value) => {289if (value > 10) console.warn('Too big!')290})291```292293## Best practices for watchers294295### Use `immediate: true` instead of duplicate initial calls296297**BAD:**298```ts299import { ref, watch, onMounted } from 'vue'300301const userId = ref(1)302303function loadUser(id) {304// ...305}306307onMounted(() => loadUser(userId.value))308watch(userId, (id) => loadUser(id))309```310311**GOOD:**312```ts313import { ref, watch } from 'vue'314315const userId = ref(1)316317watch(318userId,319(id) => loadUser(id),320{ immediate: true }321)322```323324### Clean up async effects for watchers325326When reacting to rapid changes (search boxes, filters), cancel the previous request.327328**GOOD:**329330```ts331const query = ref('')332const results = ref<string[]>([])333334watch(query, async (q, _prev, onCleanup) => {335const controller = new AbortController()336onCleanup(() => controller.abort())337338const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`, {339signal: controller.signal,340})341342results.value = await res.json()343})344```345