| name | vue-reactivity-system |
| user-invocable | false |
| description | Use when Vue reactivity system with refs, reactive, computed, and watchers. Use when managing complex state in Vue applications. |
| allowed-tools | ["Bash","Read"] |
Vue Reactivity System
Master Vue's reactivity system to build reactive, performant applications
with optimal state management and computed properties.
Reactivity Fundamentals (Proxy-based)
Vue 3 uses JavaScript Proxies for reactivity:
import { ref, reactive, isRef, isReactive, isProxy } from 'vue';
const count = ref(0);
console.log(isRef(count));
console.log(isProxy(count));
console.log(isProxy(count.value));
const state = reactive({ count: 0 });
console.log(isReactive(state));
console.log(isProxy(state));
state.count++;
count.value++;
Ref - Reactive Primitives and Objects
Basic Ref Usage
import { ref } from 'vue';
const count = ref(0);
const name = ref('John');
const isActive = ref(true);
console.log(count.value);
count.value++;
const user = ref({
name: 'John',
age: 30
});
user.value.age++;
user.value = { name: 'Jane', age: 25 };
Shallow Ref
import { shallowRef, triggerRef } from 'vue';
const state = shallowRef({
count: 0,
nested: { value: 0 }
});
state.value = { count: 1, nested: { value: 1 } };
state.value.count++;
state.value.count++;
triggerRef(state);
Custom Ref
import { customRef } from 'vue';
function useDebouncedRef<T>(value: T, delay = 200) {
let timeout: ReturnType<typeof setTimeout>;
return customRef((track, trigger) => ({
get() {
track();
return value;
},
set(newValue: T) {
clearTimeout(timeout);
timeout = setTimeout(() => {
value = newValue;
trigger();
}, delay);
}
}));
}
const searchQuery = useDebouncedRef('', 300);
searchQuery.value = 'a';
searchQuery.value = 'ab';
searchQuery.value = 'abc';
Reactive - Deep Reactive Objects
Basic Reactive Usage
import { reactive } from 'vue';
const state = reactive({
user: {
name: 'John',
profile: {
email: 'john@example.com',
settings: {
theme: 'dark'
}
}
},
posts: []
});
state.user.profile.settings.theme = 'light';
state.posts.push({ id: 1, title: 'Post' });
Shallow Reactive
import { shallowReactive } from 'vue';
const state = shallowReactive({
count: 0,
nested: { value: 0 }
});
state.count++;
state.nested.value++;
state.nested = { value: 1 };
Reactive Arrays
import { reactive } from 'vue';
const list = reactive<number[]>([]);
list.push(1);
list.pop();
list.splice(0, 1);
list.sort();
list.reverse();
const newList = reactive([1, 2, 3]);
Reactive Collections
import { reactive } from 'vue';
const map = reactive(new Map<string, number>());
map.set('count', 0);
map.delete('count');
const set = reactive(new Set<number>());
set.add(1);
set.delete(1);
const weakMap = reactive(new WeakMap());
const weakSet = reactive(new WeakSet());
Readonly - Prevent Mutations
import { reactive, readonly, isReadonly } from 'vue';
const state = reactive({ count: 0 });
const readonlyState = readonly(state);
console.log(isReadonly(readonlyState));
readonlyState.count++;
state.count++;
const deepState = reactive({
nested: { value: 0 }
});
const deepReadonly = readonly(deepState);
deepReadonly.nested.value++;
ToRef and ToRefs - Preserve Reactivity
ToRefs - Convert Reactive to Refs
import { reactive, toRefs } from 'vue';
const state = reactive({
count: 0,
name: 'John'
});
const { count, name } = state;
const { count: countRef, name: nameRef } = toRefs(state);
countRef.value++;
console.log(state.count);
ToRef - Create Ref from Property
import { reactive, toRef } from 'vue';
const state = reactive({
count: 0
});
const countRef = toRef(state, 'count');
countRef.value++;
console.log(state.count);
const missingRef = toRef(state, 'missing');
missingRef.value = 'now exists';
Unref and IsRef - Ref Utilities
import { ref, unref, isRef } from 'vue';
const count = ref(0);
const plain = 0;
console.log(unref(count));
console.log(unref(plain));
function double(value: number | Ref<number>): number {
return unref(value) * 2;
}
double(count);
double(5);
if (isRef(count)) {
console.log(count.value);
} else {
console.log(count);
}
Computed - Derived State
Basic Computed
import { ref, computed } from 'vue';
const count = ref(0);
const doubled = computed(() => count.value * 2);
console.log(doubled.value);
count.value = 5;
console.log(doubled.value);
const expensive = computed(() => {
console.log('Computing...');
return count.value * 2;
});
console.log(expensive.value);
console.log(expensive.value);
count.value = 1;
console.log(expensive.value);
Writable Computed
import { ref, computed } from 'vue';
const firstName = ref('John');
const lastName = ref('Doe');
const fullName = computed({
get() {
return `${firstName.value} ${lastName.value}`;
},
set(value) {
[firstName.value, lastName.value] = value.split(' ');
}
});
console.log(fullName.value);
fullName.value = 'Jane Smith';
console.log(firstName.value);
console.log(lastName.value);
Computed Debugging
import { ref, computed } from 'vue';
const count = ref(0);
const doubled = computed(
() => count.value * 2,
{
onTrack(e) {
console.log('Tracked:', e);
},
onTrigger(e) {
console.log('Triggered:', e);
}
}
);
Watch - React to Changes
Watch Single Source
import { ref, watch } from 'vue';
const count = ref(0);
watch(count, (newValue, oldValue) => {
console.log(`Count: ${oldValue} -> ${newValue}`);
});
watch(
count,
(newValue, oldValue) => {
console.log('Count changed');
},
{
immediate: true,
flush: 'post',
onTrack(e) { console.log('Tracked:', e); },
onTrigger(e) { console.log('Triggered:', e); }
}
);
Watch Multiple Sources
import { ref, watch } from 'vue';
const x = ref(0);
const y = ref(0);
watch(
[x, y],
([newX, newY], [oldX, oldY]) => {
console.log(`x: ${oldX} -> ${newX}`);
console.log(`y: ${oldY} -> ${newY}`);
}
);
x.value++;
y.value++;
Watch Reactive Object
import { reactive, watch } from 'vue';
const state = reactive({
count: 0,
user: { name: 'John' }
});
watch(
() => state.count,
(newValue, oldValue) => {
console.log('Count changed');
}
);
watch(
state,
(newValue, oldValue) => {
console.log('State changed');
},
{ deep: true }
);
watch(
() => state.user.name,
(newValue, oldValue) => {
console.log('Name changed');
}
);
Stop Watching
import { ref, watch } from 'vue';
const count = ref(0);
const stop = watch(count, (value) => {
console.log(`Count: ${value}`);
if (value >= 5) {
stop();
}
});
stop();
WatchEffect - Automatic Dependency Tracking
import { ref, watchEffect } from 'vue';
const count = ref(0);
const name = ref('John');
watchEffect(() => {
console.log(`${name.value}: ${count.value}`);
});
count.value++;
name.value = 'Jane';
const stop = watchEffect((onCleanup) => {
const timer = setTimeout(() => {
console.log(count.value);
}, 1000);
onCleanup(() => {
clearTimeout(timer);
});
});
stop();
WatchEffect Timing
import { ref, watchEffect, watchPostEffect, watchSyncEffect } from 'vue';
const count = ref(0);
watchEffect(() => {
console.log('Pre:', count.value);
}, { flush: 'pre' });
watchPostEffect(() => {
console.log('Post:', count.value);
});
watchSyncEffect(() => {
console.log('Sync:', count.value);
});
Effect Scope - Group Effects
import { effectScope, ref, watch } from 'vue';
const scope = effectScope();
scope.run(() => {
const count = ref(0);
watch(count, () => {
console.log('Count changed');
});
watchEffect(() => {
console.log('Effect');
});
});
scope.stop();
const parent = effectScope();
parent.run(() => {
const child = effectScope();
child.run(() => {
});
child.stop();
});
parent.stop();
Reactivity Utilities
Trigger and Scheduler
import { ref, triggerRef } from 'vue';
const count = ref(0);
count.value = 1;
triggerRef(count);
Reactive Unwrapping
import { reactive, ref } from 'vue';
const count = ref(0);
const state = reactive({
count
});
console.log(state.count);
state.count++;
const list = reactive([ref(0)]);
console.log(list[0].value);
When to Use This Skill
Use vue-reactivity-system when building modern, production-ready
applications that require:
- Complex state management patterns
- Fine-grained reactivity control
- Performance optimization through computed properties
- Advanced watching and effect patterns
- Understanding of Vue's reactive internals
- Debugging reactivity issues
- Building reactive composables
- Large-scale applications with complex data flows
Reactivity Best Practices
- Use
ref for primitives - Always wrap primitives in ref
- Use
reactive for objects - Deep reactivity for complex state
- Use
computed for derived state - Cached and reactive
- Use
watch for side effects - API calls, localStorage, etc.
- Use
watchEffect for simple effects - Auto-tracks dependencies
- Don't destructure reactive - Use
toRefs to preserve reactivity
- Use
readonly to prevent mutations - Protect shared state
- Cleanup effects properly - Return cleanup function or use
onCleanup
- Avoid deep watching everything - Performance impact
- Use
shallowRef/shallowReactive for large data - Better performance
Common Reactivity Pitfalls
- Destructuring reactive objects - Loses reactivity without
toRefs
- Forgetting
.value on refs - Common source of bugs
- Replacing reactive object - Breaks reactivity, use
ref instead
- Deep watching performance - Can be slow with large objects
- Not cleaning up watchers - Memory leaks
- Accessing refs before initialization - Can be undefined
- Mutating props - Props are readonly
- Unnecessary computed - Use regular refs if not derived
- Synchronous effects - Usually should be async
- Not understanding proxy limitations - Some operations don't track
Advanced Patterns
Reactive State Pattern
import { reactive, readonly, computed } from 'vue';
interface State {
count: number;
items: string[];
}
function createStore() {
const state = reactive<State>({
count: 0,
items: []
});
const doubled = computed(() => state.count * 2);
function increment() {
state.count++;
}
function addItem(item: string) {
state.items.push(item);
}
return {
state: readonly(state),
doubled,
increment,
addItem
};
}
const store = createStore();
Reactive Form State
import { reactive, computed, watch } from 'vue';
interface FormData {
email: string;
password: string;
}
interface FormErrors {
email?: string;
password?: string;
}
function useForm() {
const data = reactive<FormData>({
email: '',
password: ''
});
const errors = reactive<FormErrors>({});
const isValid = computed(() =>
!errors.email && !errors.password &&
data.email && data.password
);
watch(
() => data.email,
(email) => {
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
errors.email = 'Invalid email';
} else {
delete errors.email;
}
}
);
watch(
() => data.password,
() => {
(password. < ) {
errors. = ;
} {
errors.;
}
}
);
{
data,
errors,
isValid
};
}
Async Reactive State
import { ref, watchEffect } from 'vue';
interface User {
id: number;
name: string;
}
function useAsyncData<T>(
fetcher: () => Promise<T>
) {
const data = ref<T | null>(null);
const error = ref<Error | null>(null);
const loading = ref(false);
async function execute() {
loading.value = true;
error.value = null;
try {
data.value = await fetcher();
} catch (e) {
error.value = e as Error;
} finally {
loading.value = false;
}
}
watchEffect((onCleanup) => {
let cancelled = false;
execute().then(() => {
if (cancelled) {
data.value = ;
}
});
( {
cancelled = ;
});
});
{ data, error, loading, : execute };
}
Reactivity Caveats and Limitations
Property Addition/Deletion
import { reactive } from 'vue';
const state = reactive<{ count?: number }>({});
state.count = 1;
Ref Unwrapping in Templates
<script setup lang="ts">
import { ref } from 'vue';
const count = ref(0);
</script>
<template>
<!-- Auto-unwrapped in templates -->
<p>{{ count }}</p> <!-- Not count.value -->
<!-- But not in JavaScript expressions -->
<p>{{ count + 1 }}</p> <!-- Won't work! -->
<p>{{ count.value + 1 }}</p> <!-- Correct -->
</template>
Non-Reactive Values
import { reactive } from 'vue';
const state = reactive({
count: 0
});
let count = state.count;
count++;
Resources