Marketplace
vue-reactivity-system
Use when Vue reactivity system with refs, reactive, computed, and watchers. Use when managing complex state in Vue applications.
allowed_tools: Bash, Read
$ Installer
git clone https://github.com/TheBushidoCollective/han /tmp/han && cp -r /tmp/han/jutsu/jutsu-vue/skills/vue-reactivity-system ~/.claude/skills/han// tip: Run this command in your terminal to install the skill
SKILL.md
name: vue-reactivity-system 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';
// ref creates reactive wrapper
const count = ref(0);
console.log(isRef(count)); // true
console.log(isProxy(count)); // false (ref itself isn't proxy)
console.log(isProxy(count.value)); // false for primitives
// reactive creates proxy
const state = reactive({ count: 0 });
console.log(isReactive(state)); // true
console.log(isProxy(state)); // true
// Proxies track access and mutations
state.count++; // Triggers reactivity
count.value++; // Triggers reactivity
Ref - Reactive Primitives and Objects
Basic Ref Usage
import { ref } from 'vue';
// Primitives
const count = ref(0);
const name = ref('John');
const isActive = ref(true);
// Access via .value
console.log(count.value); // 0
count.value++; // Update triggers reactivity
// Objects (wrapped in proxy)
const user = ref({
name: 'John',
age: 30
});
// Nested properties are reactive
user.value.age++; // Triggers reactivity
// Can replace entire object
user.value = { name: 'Jane', age: 25 }; // Works!
Shallow Ref
import { shallowRef, triggerRef } from 'vue';
// Only .value is reactive, not nested properties
const state = shallowRef({
count: 0,
nested: { value: 0 }
});
// This triggers reactivity
state.value = { count: 1, nested: { value: 1 } };
// This does NOT trigger reactivity
state.value.count++; // No update!
// Manually trigger
state.value.count++;
triggerRef(state); // Force update
Custom Ref
import { customRef } from 'vue';
// Debounced ref
function useDebouncedRef<T>(value: T, delay = 200) {
let timeout: ReturnType<typeof setTimeout>;
return customRef((track, trigger) => ({
get() {
track(); // Tell Vue to track this
return value;
},
set(newValue: T) {
clearTimeout(timeout);
timeout = setTimeout(() => {
value = newValue;
trigger(); // Tell Vue to re-render
}, delay);
}
}));
}
// Usage
const searchQuery = useDebouncedRef('', 300);
// Updates are debounced
searchQuery.value = 'a'; // Doesn't trigger immediately
searchQuery.value = 'ab'; // Still waiting
searchQuery.value = 'abc'; // Triggers after 300ms
Reactive - Deep Reactive Objects
Basic Reactive Usage
import { reactive } from 'vue';
// Create deep reactive object
const state = reactive({
user: {
name: 'John',
profile: {
email: 'john@example.com',
settings: {
theme: 'dark'
}
}
},
posts: []
});
// All nested properties are reactive
state.user.profile.settings.theme = 'light'; // Triggers reactivity
state.posts.push({ id: 1, title: 'Post' }); // Triggers reactivity
Shallow Reactive
import { shallowReactive } from 'vue';
// Only root-level properties are reactive
const state = shallowReactive({
count: 0,
nested: { value: 0 }
});
// This triggers reactivity
state.count++; // Works
// This does NOT trigger reactivity
state.nested.value++; // No update!
// But replacing works
state.nested = { value: 1 }; // Triggers reactivity
Reactive Arrays
import { reactive } from 'vue';
const list = reactive<number[]>([]);
// Mutating methods trigger reactivity
list.push(1); // Reactive
list.pop(); // Reactive
list.splice(0, 1); // Reactive
list.sort(); // Reactive
list.reverse(); // Reactive
// Replacement triggers reactivity
const newList = reactive([1, 2, 3]);
Reactive Collections
import { reactive } from 'vue';
// Map
const map = reactive(new Map<string, number>());
map.set('count', 0); // Reactive
map.delete('count'); // Reactive
// Set
const set = reactive(new Set<number>());
set.add(1); // Reactive
set.delete(1); // Reactive
// WeakMap and WeakSet
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)); // true
// Cannot mutate
readonlyState.count++; // Warning in dev mode
// Original is still mutable
state.count++; // Works, updates readonly view too
// Deep readonly
const deepState = reactive({
nested: { value: 0 }
});
const deepReadonly = readonly(deepState);
deepReadonly.nested.value++; // Warning! Deep readonly
ToRef and ToRefs - Preserve Reactivity
ToRefs - Convert Reactive to Refs
import { reactive, toRefs } from 'vue';
const state = reactive({
count: 0,
name: 'John'
});
// Destructuring loses reactivity
const { count, name } = state; // NOT reactive!
// Use toRefs to preserve reactivity
const { count: countRef, name: nameRef } = toRefs(state);
// Now reactive
countRef.value++; // Updates state.count
console.log(state.count); // 1
ToRef - Create Ref from Property
import { reactive, toRef } from 'vue';
const state = reactive({
count: 0
});
// Create ref to specific property
const countRef = toRef(state, 'count');
countRef.value++; // Updates state.count
console.log(state.count); // 1
// Non-existent properties
const missingRef = toRef(state, 'missing');
missingRef.value = 'now exists'; // Adds to state!
Unref and IsRef - Ref Utilities
import { ref, unref, isRef } from 'vue';
const count = ref(0);
const plain = 0;
// unref: unwrap if ref, return value otherwise
console.log(unref(count)); // 0
console.log(unref(plain)); // 0
// Useful for handling ref or value
function double(value: number | Ref<number>): number {
return unref(value) * 2;
}
double(count); // 0
double(5); // 10
// isRef: check if value is ref
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); // 0
count.value = 5;
console.log(doubled.value); // 10
// Computed is cached
const expensive = computed(() => {
console.log('Computing...');
return count.value * 2;
});
console.log(expensive.value); // Computing... 0
console.log(expensive.value); // 0 (cached, no log)
count.value = 1;
console.log(expensive.value); // Computing... 2
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); // John Doe
fullName.value = 'Jane Smith';
console.log(firstName.value); // Jane
console.log(lastName.value); // Smith
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);
// Basic watch
watch(count, (newValue, oldValue) => {
console.log(`Count: ${oldValue} -> ${newValue}`);
});
// With options
watch(
count,
(newValue, oldValue) => {
console.log('Count changed');
},
{
immediate: true, // Run immediately
flush: 'post', // Timing: 'pre' | 'post' | 'sync'
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 array of sources
watch(
[x, y],
([newX, newY], [oldX, oldY]) => {
console.log(`x: ${oldX} -> ${newX}`);
console.log(`y: ${oldY} -> ${newY}`);
}
);
// Trigger when any changes
x.value++; // Logs
y.value++; // Logs
Watch Reactive Object
import { reactive, watch } from 'vue';
const state = reactive({
count: 0,
user: { name: 'John' }
});
// Watch getter
watch(
() => state.count,
(newValue, oldValue) => {
console.log('Count changed');
}
);
// Deep watch entire object
watch(
state,
(newValue, oldValue) => {
console.log('State changed');
},
{ deep: true }
);
// Watch specific nested property
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}`);
// Stop watching when count reaches 5
if (value >= 5) {
stop();
}
});
// Or stop externally
stop();
WatchEffect - Automatic Dependency Tracking
import { ref, watchEffect } from 'vue';
const count = ref(0);
const name = ref('John');
// Automatically tracks dependencies
watchEffect(() => {
console.log(`${name.value}: ${count.value}`);
});
// Logs: John: 0
count.value++; // Logs: John: 1
name.value = 'Jane'; // Logs: Jane: 1
// Cleanup
const stop = watchEffect((onCleanup) => {
const timer = setTimeout(() => {
console.log(count.value);
}, 1000);
// Register cleanup
onCleanup(() => {
clearTimeout(timer);
});
});
// Stop watching
stop();
WatchEffect Timing
import { ref, watchEffect, watchPostEffect, watchSyncEffect } from 'vue';
const count = ref(0);
// Default: 'pre' - before component update
watchEffect(() => {
console.log('Pre:', count.value);
}, { flush: 'pre' });
// 'post' - after component update (access updated DOM)
watchPostEffect(() => {
console.log('Post:', count.value);
// Can access updated DOM
});
// 'sync' - synchronous (use sparingly!)
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');
});
});
// Stop all effects in scope
scope.stop();
// Nested scopes
const parent = effectScope();
parent.run(() => {
const child = effectScope();
child.run(() => {
// Child effects
});
// Stop child only
child.stop();
});
// Stop parent (and all children)
parent.stop();
Reactivity Utilities
Trigger and Scheduler
import { ref, triggerRef } from 'vue';
const count = ref(0);
// Manually trigger updates
count.value = 1;
triggerRef(count); // Force update even if value didn't change
Reactive Unwrapping
import { reactive, ref } from 'vue';
const count = ref(0);
const state = reactive({
// Refs are auto-unwrapped in reactive
count
});
// No .value needed
console.log(state.count); // 0 (not state.count.value)
state.count++; // Works
// But in arrays, not unwrapped
const list = reactive([ref(0)]);
console.log(list[0].value); // Must use .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
reffor primitives - Always wrap primitives in ref - Use
reactivefor objects - Deep reactivity for complex state - Use
computedfor derived state - Cached and reactive - Use
watchfor side effects - API calls, localStorage, etc. - Use
watchEffectfor simple effects - Auto-tracks dependencies - Don't destructure reactive - Use
toRefsto preserve reactivity - Use
readonlyto prevent mutations - Protect shared state - Cleanup effects properly - Return cleanup function or use
onCleanup - Avoid deep watching everything - Performance impact
- Use
shallowRef/shallowReactivefor large data - Better performance
Common Reactivity Pitfalls
- Destructuring reactive objects - Loses reactivity without
toRefs - Forgetting
.valueon refs - Common source of bugs - Replacing reactive object - Breaks reactivity, use
refinstead - 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: []
});
// Computed
const doubled = computed(() => state.count * 2);
// Actions
function increment() {
state.count++;
}
function addItem(item: string) {
state.items.push(item);
}
// Expose readonly state
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
);
// Validate on change
watch(
() => data.email,
(email) => {
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
errors.email = 'Invalid email';
} else {
delete errors.email;
}
}
);
watch(
() => data.password,
(password) => {
if (password.length < 8) {
errors.password = 'Must be 8+ characters';
} else {
delete errors.password;
}
}
);
return {
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 = null;
}
});
onCleanup(() => {
cancelled = true;
});
});
return { data, error, loading, refetch: execute };
}
Reactivity Caveats and Limitations
Property Addition/Deletion
import { reactive } from 'vue';
const state = reactive<{ count?: number }>({});
// Adding new property is reactive
state.count = 1; // Reactive
// But TypeScript won't know about it unless typed
// Solution: Define all properties upfront or use proper types
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';
// Primitive values in reactive are still reactive
const state = reactive({
count: 0 // Reactive
});
// But extracting loses reactivity
let count = state.count; // Not reactive
count++; // Doesn't update state
Resources
Repository

TheBushidoCollective
Author
TheBushidoCollective/han/jutsu/jutsu-vue/skills/vue-reactivity-system
47
Stars
5
Forks
Updated6d ago
Added1w ago