Reactivity In Depth
Basic Reactivity Example
Section titled “Basic Reactivity Example”let A0 = 1let A1 = 2let A2 = A0 + A1console.log(A2) // 3A0 = 2console.log(A2) // Still 3When we change A0, A2 doesn’t automatically update. To create reactivity:
let A2function update() { A2 = A0 + A1}whenDepsChange(update)How Vue Implements Reactivity
Section titled “How Vue Implements Reactivity”Vue uses JavaScript’s Proxy to detect when reactive properties are accessed or modified:
function reactive(obj) { return new Proxy(obj, { get(target, key) { track(target, key) return target[key] }, set(target, key, value) { target[key] = value trigger(target, key) } })}For primitive values, Vue uses a ref object:
function ref(value) { const refObject = { get value() { track(refObject, 'value') return value }, set value(newValue) { value = newValue trigger(refObject, 'value') } } return refObject}Tracking Dependencies
Section titled “Tracking Dependencies”// This will be set right before an effect is about to be runlet activeEffect
function track(target, key) { if (activeEffect) { const effects = getSubscribersForProperty(target, key) effects.add(activeEffect) }}
function trigger(target, key) { const effects = getSubscribersForProperty(target, key) effects.forEach((effect) => effect())}The dependency tracking system uses a WeakMap<target, Map<key, Set<effect>>> structure.
Effect Registration
Section titled “Effect Registration”function whenDepsChange(update) { const effect = () => { activeEffect = effect update() activeEffect = null } effect()}Vue’s API Example
Section titled “Vue’s API Example”Using ref and watchEffect:
import { ref, watchEffect } from 'vue'
const A0 = ref(0)const A1 = ref(1)const A2 = ref()
watchEffect(() => { // tracks A0 and A1 A2.value = A0.value + A1.value})
// triggers the effectA0.value = 2Using the computed property:
import { ref, computed } from 'vue'
const A0 = ref(0)const A1 = ref(1)const A2 = computed(() => A0.value + A1.value)
A0.value = 2DOM Updates Example
Section titled “DOM Updates Example”Vue automatically batches DOM updates:
import { ref, watchEffect } from 'vue'
const count = ref(0)
watchEffect(() => { document.body.innerHTML = `Count is: ${count.value}`})
// updates the DOMcount.value++Debugging Reactivity
Section titled “Debugging Reactivity”Vue provides hooks for debugging reactivity:
<script setup>import { onRenderTracked, onRenderTriggered } from 'vue'
onRenderTracked((event) => { debugger})
onRenderTriggered((event) => { debugger})</script>The debugging event object:
type DebuggerEvent = { effect: ReactiveEffect target: object type: | TrackOpTypes /* 'get' | 'has' | 'iterate' */ | TriggerOpTypes /* 'set' | 'add' | 'delete' | 'clear' */ key: any newValue?: any oldValue?: any oldTarget?: Map<any, any> | Set<any>}Debugging computed properties:
const plusOne = computed(() => count.value + 1, { onTrack(e) { // triggered when count.value is tracked as a dependency debugger }, onTrigger(e) { // triggered when count.value is mutated debugger }})
// access plusOne, should trigger onTrackconsole.log(plusOne.value)
// mutate count.value, should trigger onTriggercount.value++You can also debug watchers:
watch(source, callback, { onTrack(e) { debugger }, onTrigger(e) { debugger }})
watchEffect(callback, { onTrack(e) { debugger }, onTrigger(e) { debugger }})Integration with External State Management Libraries
Section titled “Integration with External State Management Libraries”Using shallowRef with Immer:
import { produce } from 'immer'import { shallowRef } from 'vue'
export function useImmer(baseState) { const state = shallowRef(baseState) const update = (updater) => { state.value = produce(state.value, updater) } return [state, update]}Integration with XState:
import { createMachine, interpret } from 'xstate'import { shallowRef } from 'vue'
export function useMachine(options) { const machine = createMachine(options) const state = shallowRef(machine.initialState) const service = interpret(machine) .onTransition((newState) => (state.value = newState)) .start() const send = (event) => service.send(event) return [state, send]}Alternative API Patterns
Section titled “Alternative API Patterns”Vue’s ref pattern requires using the .value property, while some libraries like SolidJS use a different approach:
// SolidJS styleconst [count, setCount] = createSignal(0)count() // access the valuesetCount(1) // update the valueYou can implement a similar API in Vue:
import { shallowRef, triggerRef } from 'vue'
export function createSignal(value, options) { const r = shallowRef(value) const get = () => r.value const set = (v) => { r.value = typeof v === 'function' ? v(r.value) : v if (options?.equals === false) triggerRef(r) } return [get, set]}Alternative signal implementation:
import { shallowRef } from 'vue'
export function signal(initialValue) { const r = shallowRef(initialValue) const s = () => r.value s.set = (value) => { r.value = value } s.update = (updater) => { r.value = updater(r.value) } return s}
// Usage:const count = signal(0)count() // access the valuecount.set(1) // set new valuecount.update((v) => v + 1) // update based on previous valueThe function call approach () compared to using .value offers different API ergonomics depending on preferences.