--- name: core-new-apis description: Vue 3 reactivity system, lifecycle hooks, and composable patterns --- # Reactivity, Lifecycle & Composables ## Reactivity ### ref vs shallowRef ```ts import { ref, shallowRef } from 'vue' // ref - deep reactivity (tracks nested changes) const user = ref({ name: 'John', profile: { age: 30 } }) user.value.profile.age = 31 // Triggers reactivity // shallowRef - only .value assignment triggers reactivity (better performance) const data = shallowRef({ items: [] }) data.value.items.push('new') // Does NOT trigger reactivity data.value = { items: ['new'] } // Triggers reactivity ``` **Prefer `shallowRef`** for large data structures or when deep reactivity is unnecessary. ### computed ```ts import { ref, computed } from 'vue' const count = ref(0) // Read-only computed const doubled = computed(() => count.value * 2) // Writable computed const plusOne = computed({ get: () => count.value + 1, set: (val) => { count.value = val - 1 } }) ``` ### reactive & readonly ```ts import { reactive, readonly } from 'vue' const state = reactive({ count: 0, nested: { value: 1 } }) state.count++ // Reactive const readonlyState = readonly(state) readonlyState.count++ // Warning, mutation blocked ``` Note: `reactive()` loses reactivity on destructuring. Use `ref()` or `toRefs()`. ## Watchers ### watch ```ts import { ref, watch } from 'vue' const count = ref(0) // Watch single ref watch(count, (newVal, oldVal) => { console.log(`Changed from ${oldVal} to ${newVal}`) }) // Watch getter watch( () => props.id, (id) => fetchData(id), { immediate: true } ) // Watch multiple sources watch([firstName, lastName], ([first, last]) => { fullName.value = `${first} ${last}` }) // Deep watch with depth limit (Vue 3.5+) watch(state, callback, { deep: 2 }) // Once (Vue 3.4+) watch(source, callback, { once: true }) ``` ### watchEffect Runs immediately and auto-tracks dependencies. ```ts import { ref, watchEffect, onWatcherCleanup } from 'vue' const id = ref(1) watchEffect(async () => { const controller = new AbortController() // Cleanup on re-run or unmount (Vue 3.5+) onWatcherCleanup(() => controller.abort()) const res = await fetch(`/api/${id.value}`, { signal: controller.signal }) data.value = await res.json() }) // Pause/resume (Vue 3.5+) const { pause, resume, stop } = watchEffect(() => {}) pause() resume() stop() ``` ### Flush Timing ```ts // 'pre' (default) - before component update // 'post' - after component update (access updated DOM) // 'sync' - immediate, use with caution watch(source, callback, { flush: 'post' }) watchPostEffect(() => {}) // Alias for flush: 'post' ``` ## Lifecycle Hooks ```ts import { onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted, onErrorCaptured, onActivated, // KeepAlive onDeactivated, // KeepAlive onServerPrefetch // SSR only } from 'vue' onMounted(() => { console.log('DOM is ready') }) onUnmounted(() => { // Cleanup timers, listeners, etc. }) // Error boundary onErrorCaptured((err, instance, info) => { console.error(err) return false // Stop propagation }) ``` ## Effect Scope Group reactive effects for batch disposal. ```ts import { effectScope, onScopeDispose } from 'vue' const scope = effectScope() scope.run(() => { const count = ref(0) const doubled = computed(() => count.value * 2) watch(count, () => console.log(count.value)) // Cleanup when scope stops onScopeDispose(() => { console.log('Scope disposed') }) }) // Dispose all effects scope.stop() ``` ## Composables Composables are functions that encapsulate stateful logic using Composition API. ### Naming Convention - Start with `use`: `useMouse`, `useFetch`, `useCounter` ### Pattern ```ts // composables/useMouse.ts import { ref, onMounted, onUnmounted } from 'vue' export function useMouse() { const x = ref(0) const y = ref(0) const update = (e: MouseEvent) => { x.value = e.pageX y.value = e.pageY } onMounted(() => window.addEventListener('mousemove', update)) onUnmounted(() => window.removeEventListener('mousemove', update)) return { x, y } } ``` ### Accept Reactive Input Use `toValue()` (Vue 3.3+) to normalize refs, getters, or plain values. ```ts import { ref, watchEffect, toValue, type MaybeRefOrGetter } from 'vue' export function useFetch(url: MaybeRefOrGetter) { const data = ref(null) const error = ref(null) watchEffect(async () => { data.value = null error.value = null try { const res = await fetch(toValue(url)) data.value = await res.json() } catch (e) { error.value = e } }) return { data, error } } // Usage - all work: useFetch('/api/users') useFetch(urlRef) useFetch(() => `/api/users/${props.id}`) ``` ### Return Refs (Not Reactive) Always return plain object with refs for destructuring compatibility. ```ts // Good - preserves reactivity when destructured return { x, y } // Bad - loses reactivity when destructured return reactive({ x, y }) ```